{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Configurations for Colab"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 08. Rainbow\n",
    "\n",
    "[M. Hessel et al., \"Rainbow: Combining Improvements in Deep Reinforcement Learning.\" arXiv preprint arXiv:1710.02298, 2017.](https://arxiv.org/pdf/1710.02298.pdf)\n",
    "\n",
    "We will integrate all the following seven components into a single integrated agent, which is called Rainbow!\n",
    "\n",
    "1. DQN\n",
    "2. Double DQN\n",
    "3. Prioritized Experience Replay\n",
    "4. Dueling Network\n",
    "5. Noisy Network\n",
    "6. Categorical DQN\n",
    "7. N-step Learning\n",
    "\n",
    "This method shows an impressive performance on the Atari 2600 benchmark, both in terms of data efficiency and final performance. \n",
    "\n",
    "![rainbow](https://user-images.githubusercontent.com/14961526/60591412-61748100-9dd9-11e9-84fb-076c7a61fbab.png)\n",
    "\n",
    "However, the integration is not so simple because some of components are not independent each other, so we will look into a number of points that people especailly feel confused.\n",
    "\n",
    "1. Noisy Network <-> Dueling Network\n",
    "2. Dueling Network <-> Categorical DQN\n",
    "3. Categorical DQN <-> Double DQN"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "import os\n",
    "import random\n",
    "from collections import deque\n",
    "from typing import Deque, Dict, List, Tuple\n",
    "\n",
    "import gymnasium as gym\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torch.optim as optim\n",
    "from IPython.display import clear_output\n",
    "from torch.nn.utils import clip_grad_norm_\n",
    "\n",
    "from segment_tree import MinSegmentTree, SumSegmentTree"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Replay buffer\n",
    "\n",
    "Same as the basic N-step buffer. \n",
    "\n",
    "(Please see *01.dqn.ipynb*, *07.n_step_learning.ipynb* for detailed description about the basic (n-step) replay buffer.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ReplayBuffer:\n",
    "    \"\"\"A simple numpy replay buffer.\"\"\"\n",
    "\n",
    "    def __init__(\n",
    "        self, \n",
    "        obs_dim: int, \n",
    "        size: int, \n",
    "        batch_size: int = 32, \n",
    "        n_step: int = 1, \n",
    "        gamma: float = 0.99\n",
    "    ):\n",
    "        self.obs_buf = np.zeros([size, obs_dim], dtype=np.float32)\n",
    "        self.next_obs_buf = np.zeros([size, obs_dim], dtype=np.float32)\n",
    "        self.acts_buf = np.zeros([size], dtype=np.float32)\n",
    "        self.rews_buf = np.zeros([size], dtype=np.float32)\n",
    "        self.done_buf = np.zeros(size, dtype=np.float32)\n",
    "        self.max_size, self.batch_size = size, batch_size\n",
    "        self.ptr, self.size, = 0, 0\n",
    "        \n",
    "        # for N-step Learning\n",
    "        self.n_step_buffer = deque(maxlen=n_step)\n",
    "        self.n_step = n_step\n",
    "        self.gamma = gamma\n",
    "\n",
    "    def store(\n",
    "        self, \n",
    "        obs: np.ndarray, \n",
    "        act: np.ndarray, \n",
    "        rew: float, \n",
    "        next_obs: np.ndarray, \n",
    "        done: bool,\n",
    "    ) -> Tuple[np.ndarray, np.ndarray, float, np.ndarray, bool]:\n",
    "        transition = (obs, act, rew, next_obs, done)\n",
    "        self.n_step_buffer.append(transition)\n",
    "\n",
    "        # single step transition is not ready\n",
    "        if len(self.n_step_buffer) < self.n_step:\n",
    "            return ()\n",
    "        \n",
    "        # make a n-step transition\n",
    "        rew, next_obs, done = self._get_n_step_info(\n",
    "            self.n_step_buffer, self.gamma\n",
    "        )\n",
    "        obs, act = self.n_step_buffer[0][:2]\n",
    "        \n",
    "        self.obs_buf[self.ptr] = obs\n",
    "        self.next_obs_buf[self.ptr] = next_obs\n",
    "        self.acts_buf[self.ptr] = act\n",
    "        self.rews_buf[self.ptr] = rew\n",
    "        self.done_buf[self.ptr] = done\n",
    "        self.ptr = (self.ptr + 1) % self.max_size\n",
    "        self.size = min(self.size + 1, self.max_size)\n",
    "        \n",
    "        return self.n_step_buffer[0]\n",
    "\n",
    "    def sample_batch(self) -> Dict[str, np.ndarray]:\n",
    "        idxs = np.random.choice(self.size, size=self.batch_size, replace=False)\n",
    "\n",
    "        return dict(\n",
    "            obs=self.obs_buf[idxs],\n",
    "            next_obs=self.next_obs_buf[idxs],\n",
    "            acts=self.acts_buf[idxs],\n",
    "            rews=self.rews_buf[idxs],\n",
    "            done=self.done_buf[idxs],\n",
    "            # for N-step Learning\n",
    "            indices=idxs,\n",
    "        )\n",
    "    \n",
    "    def sample_batch_from_idxs(\n",
    "        self, idxs: np.ndarray\n",
    "    ) -> Dict[str, np.ndarray]:\n",
    "        # for N-step Learning\n",
    "        return dict(\n",
    "            obs=self.obs_buf[idxs],\n",
    "            next_obs=self.next_obs_buf[idxs],\n",
    "            acts=self.acts_buf[idxs],\n",
    "            rews=self.rews_buf[idxs],\n",
    "            done=self.done_buf[idxs],\n",
    "        )\n",
    "    \n",
    "    def _get_n_step_info(\n",
    "        self, n_step_buffer: Deque, gamma: float\n",
    "    ) -> Tuple[np.int64, np.ndarray, bool]:\n",
    "        \"\"\"Return n step rew, next_obs, and done.\"\"\"\n",
    "        # info of the last transition\n",
    "        rew, next_obs, done = n_step_buffer[-1][-3:]\n",
    "\n",
    "        for transition in reversed(list(n_step_buffer)[:-1]):\n",
    "            r, n_o, d = transition[-3:]\n",
    "\n",
    "            rew = r + gamma * rew * (1 - d)\n",
    "            next_obs, done = (n_o, d) if d else (next_obs, done)\n",
    "\n",
    "        return rew, next_obs, done\n",
    "\n",
    "    def __len__(self) -> int:\n",
    "        return self.size"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Prioritized replay Buffer\n",
    "\n",
    "`store` method returns boolean in order to inform if a N-step transition has been generated.\n",
    "\n",
    "(Please see *02.per.ipynb* for detailed description about PER.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class PrioritizedReplayBuffer(ReplayBuffer):\n",
    "    \"\"\"Prioritized Replay buffer.\n",
    "    \n",
    "    Attributes:\n",
    "        max_priority (float): max priority\n",
    "        tree_ptr (int): next index of tree\n",
    "        alpha (float): alpha parameter for prioritized replay buffer\n",
    "        sum_tree (SumSegmentTree): sum tree for prior\n",
    "        min_tree (MinSegmentTree): min tree for min prior to get max weight\n",
    "        \n",
    "    \"\"\"\n",
    "    \n",
    "    def __init__(\n",
    "        self, \n",
    "        obs_dim: int, \n",
    "        size: int, \n",
    "        batch_size: int = 32, \n",
    "        alpha: float = 0.6,\n",
    "        n_step: int = 1, \n",
    "        gamma: float = 0.99,\n",
    "    ):\n",
    "        \"\"\"Initialization.\"\"\"\n",
    "        assert alpha >= 0\n",
    "        \n",
    "        super(PrioritizedReplayBuffer, self).__init__(\n",
    "            obs_dim, size, batch_size, n_step, gamma\n",
    "        )\n",
    "        self.max_priority, self.tree_ptr = 1.0, 0\n",
    "        self.alpha = alpha\n",
    "        \n",
    "        # capacity must be positive and a power of 2.\n",
    "        tree_capacity = 1\n",
    "        while tree_capacity < self.max_size:\n",
    "            tree_capacity *= 2\n",
    "\n",
    "        self.sum_tree = SumSegmentTree(tree_capacity)\n",
    "        self.min_tree = MinSegmentTree(tree_capacity)\n",
    "        \n",
    "    def store(\n",
    "        self, \n",
    "        obs: np.ndarray, \n",
    "        act: int, \n",
    "        rew: float, \n",
    "        next_obs: np.ndarray, \n",
    "        done: bool,\n",
    "    ) -> Tuple[np.ndarray, np.ndarray, float, np.ndarray, bool]:\n",
    "        \"\"\"Store experience and priority.\"\"\"\n",
    "        transition = super().store(obs, act, rew, next_obs, done)\n",
    "        \n",
    "        if transition:\n",
    "            self.sum_tree[self.tree_ptr] = self.max_priority ** self.alpha\n",
    "            self.min_tree[self.tree_ptr] = self.max_priority ** self.alpha\n",
    "            self.tree_ptr = (self.tree_ptr + 1) % self.max_size\n",
    "        \n",
    "        return transition\n",
    "\n",
    "    def sample_batch(self, beta: float = 0.4) -> Dict[str, np.ndarray]:\n",
    "        \"\"\"Sample a batch of experiences.\"\"\"\n",
    "        assert len(self) >= self.batch_size\n",
    "        assert beta > 0\n",
    "        \n",
    "        indices = self._sample_proportional()\n",
    "        \n",
    "        obs = self.obs_buf[indices]\n",
    "        next_obs = self.next_obs_buf[indices]\n",
    "        acts = self.acts_buf[indices]\n",
    "        rews = self.rews_buf[indices]\n",
    "        done = self.done_buf[indices]\n",
    "        weights = np.array([self._calculate_weight(i, beta) for i in indices])\n",
    "        \n",
    "        return dict(\n",
    "            obs=obs,\n",
    "            next_obs=next_obs,\n",
    "            acts=acts,\n",
    "            rews=rews,\n",
    "            done=done,\n",
    "            weights=weights,\n",
    "            indices=indices,\n",
    "        )\n",
    "        \n",
    "    def update_priorities(self, indices: List[int], priorities: np.ndarray):\n",
    "        \"\"\"Update priorities of sampled transitions.\"\"\"\n",
    "        assert len(indices) == len(priorities)\n",
    "\n",
    "        for idx, priority in zip(indices, priorities):\n",
    "            assert priority > 0\n",
    "            assert 0 <= idx < len(self)\n",
    "\n",
    "            self.sum_tree[idx] = priority ** self.alpha\n",
    "            self.min_tree[idx] = priority ** self.alpha\n",
    "\n",
    "            self.max_priority = max(self.max_priority, priority)\n",
    "            \n",
    "    def _sample_proportional(self) -> List[int]:\n",
    "        \"\"\"Sample indices based on proportions.\"\"\"\n",
    "        indices = []\n",
    "        p_total = self.sum_tree.sum(0, len(self) - 1)\n",
    "        segment = p_total / self.batch_size\n",
    "        \n",
    "        for i in range(self.batch_size):\n",
    "            a = segment * i\n",
    "            b = segment * (i + 1)\n",
    "            upperbound = random.uniform(a, b)\n",
    "            idx = self.sum_tree.retrieve(upperbound)\n",
    "            indices.append(idx)\n",
    "            \n",
    "        return indices\n",
    "    \n",
    "    def _calculate_weight(self, idx: int, beta: float):\n",
    "        \"\"\"Calculate the weight of the experience at idx.\"\"\"\n",
    "        # get max weight\n",
    "        p_min = self.min_tree.min() / self.sum_tree.sum()\n",
    "        max_weight = (p_min * len(self)) ** (-beta)\n",
    "        \n",
    "        # calculate weights\n",
    "        p_sample = self.sum_tree[idx] / self.sum_tree.sum()\n",
    "        weight = (p_sample * len(self)) ** (-beta)\n",
    "        weight = weight / max_weight\n",
    "        \n",
    "        return weight"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Noisy Layer\n",
    "\n",
    "Please see *05.noisy_net.ipynb* for detailed description.\n",
    "\n",
    "**References:**\n",
    "\n",
    "- https://github.com/higgsfield/RL-Adventure/blob/master/5.noisy%20dqn.ipynb\n",
    "- https://github.com/Kaixhin/Rainbow/blob/master/model.py"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NoisyLinear(nn.Module):\n",
    "    \"\"\"Noisy linear module for NoisyNet.\n",
    "\n",
    "    Attributes:\n",
    "        in_features (int): input size of linear module\n",
    "        out_features (int): output size of linear module\n",
    "        std_init (float): initial std value\n",
    "        weight_mu (nn.Parameter): mean value weight parameter\n",
    "        weight_sigma (nn.Parameter): std value weight parameter\n",
    "        bias_mu (nn.Parameter): mean value bias parameter\n",
    "        bias_sigma (nn.Parameter): std value bias parameter\n",
    "        \n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(\n",
    "        self, \n",
    "        in_features: int, \n",
    "        out_features: int, \n",
    "        std_init: float = 0.5,\n",
    "    ):\n",
    "        \"\"\"Initialization.\"\"\"\n",
    "        super(NoisyLinear, self).__init__()\n",
    "        \n",
    "        self.in_features = in_features\n",
    "        self.out_features = out_features\n",
    "        self.std_init = std_init\n",
    "\n",
    "        self.weight_mu = nn.Parameter(torch.Tensor(out_features, in_features))\n",
    "        self.weight_sigma = nn.Parameter(\n",
    "            torch.Tensor(out_features, in_features)\n",
    "        )\n",
    "        self.register_buffer(\n",
    "            \"weight_epsilon\", torch.Tensor(out_features, in_features)\n",
    "        )\n",
    "\n",
    "        self.bias_mu = nn.Parameter(torch.Tensor(out_features))\n",
    "        self.bias_sigma = nn.Parameter(torch.Tensor(out_features))\n",
    "        self.register_buffer(\"bias_epsilon\", torch.Tensor(out_features))\n",
    "\n",
    "        self.reset_parameters()\n",
    "        self.reset_noise()\n",
    "\n",
    "    def reset_parameters(self):\n",
    "        \"\"\"Reset trainable network parameters (factorized gaussian noise).\"\"\"\n",
    "        mu_range = 1 / math.sqrt(self.in_features)\n",
    "        self.weight_mu.data.uniform_(-mu_range, mu_range)\n",
    "        self.weight_sigma.data.fill_(\n",
    "            self.std_init / math.sqrt(self.in_features)\n",
    "        )\n",
    "        self.bias_mu.data.uniform_(-mu_range, mu_range)\n",
    "        self.bias_sigma.data.fill_(\n",
    "            self.std_init / math.sqrt(self.out_features)\n",
    "        )\n",
    "\n",
    "    def reset_noise(self):\n",
    "        \"\"\"Make new noise.\"\"\"\n",
    "        epsilon_in = self.scale_noise(self.in_features)\n",
    "        epsilon_out = self.scale_noise(self.out_features)\n",
    "\n",
    "        # outer product\n",
    "        self.weight_epsilon.copy_(epsilon_out.ger(epsilon_in))\n",
    "        self.bias_epsilon.copy_(epsilon_out)\n",
    "\n",
    "    def forward(self, x: torch.Tensor) -> torch.Tensor:\n",
    "        \"\"\"Forward method implementation.\n",
    "        \n",
    "        We don't use separate statements on train / eval mode.\n",
    "        It doesn't show remarkable difference of performance.\n",
    "        \"\"\"\n",
    "        return F.linear(\n",
    "            x,\n",
    "            self.weight_mu + self.weight_sigma * self.weight_epsilon,\n",
    "            self.bias_mu + self.bias_sigma * self.bias_epsilon,\n",
    "        )\n",
    "    \n",
    "    @staticmethod\n",
    "    def scale_noise(size: int) -> torch.Tensor:\n",
    "        \"\"\"Set scale to make noise (factorized gaussian noise).\"\"\"\n",
    "        x = torch.randn(size)\n",
    "\n",
    "        return x.sign().mul(x.abs().sqrt())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## NoisyNet + DuelingNet + Categorical DQN\n",
    "\n",
    "#### NoisyNet + DuelingNet\n",
    "\n",
    "NoisyLinear is employed for the last two layers of advantage and value layers. The noise should be reset at evey update step.\n",
    "\n",
    "#### DuelingNet + Categorical DQN\n",
    "\n",
    "The dueling network architecture is adapted for use with return distributions. The network has a shared representation, which is then fed into a value stream with atom_size outputs, and into an advantage stream with atom_size × out_dim outputs. For each atom, the value and advantage streams are aggregated, as in dueling DQN, and then passed through a softmax layer to obtain the normalized parametric distributions used to estimate the returns’ distributions.\n",
    "\n",
    "```\n",
    "        advantage = self.advantage_layer(adv_hid).view(-1, self.out_dim, self.atom_size)\n",
    "        value = self.value_layer(val_hid).view(-1, 1, self.atom_size)\n",
    "        q_atoms = value + advantage - advantage.mean(dim=1, keepdim=True)\n",
    "        \n",
    "        dist = F.softmax(q_atoms, dim=-1)\n",
    "```\n",
    "\n",
    "(Please see *04.dueling.ipynb*, *05.noisy_net.ipynb*, *06.categorical_dqn.ipynb* for detailed description of each component's network architecture.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Network(nn.Module):\n",
    "    def __init__(\n",
    "        self, \n",
    "        in_dim: int, \n",
    "        out_dim: int, \n",
    "        atom_size: int, \n",
    "        support: torch.Tensor\n",
    "    ):\n",
    "        \"\"\"Initialization.\"\"\"\n",
    "        super(Network, self).__init__()\n",
    "        \n",
    "        self.support = support\n",
    "        self.out_dim = out_dim\n",
    "        self.atom_size = atom_size\n",
    "\n",
    "        # set common feature layer\n",
    "        self.feature_layer = nn.Sequential(\n",
    "            nn.Linear(in_dim, 128), \n",
    "            nn.ReLU(),\n",
    "        )\n",
    "        \n",
    "        # set advantage layer\n",
    "        self.advantage_hidden_layer = NoisyLinear(128, 128)\n",
    "        self.advantage_layer = NoisyLinear(128, out_dim * atom_size)\n",
    "\n",
    "        # set value layer\n",
    "        self.value_hidden_layer = NoisyLinear(128, 128)\n",
    "        self.value_layer = NoisyLinear(128, atom_size)\n",
    "\n",
    "    def forward(self, x: torch.Tensor) -> torch.Tensor:\n",
    "        \"\"\"Forward method implementation.\"\"\"\n",
    "        dist = self.dist(x)\n",
    "        q = torch.sum(dist * self.support, dim=2)\n",
    "        \n",
    "        return q\n",
    "    \n",
    "    def dist(self, x: torch.Tensor) -> torch.Tensor:\n",
    "        \"\"\"Get distribution for atoms.\"\"\"\n",
    "        feature = self.feature_layer(x)\n",
    "        adv_hid = F.relu(self.advantage_hidden_layer(feature))\n",
    "        val_hid = F.relu(self.value_hidden_layer(feature))\n",
    "        \n",
    "        advantage = self.advantage_layer(adv_hid).view(\n",
    "            -1, self.out_dim, self.atom_size\n",
    "        )\n",
    "        value = self.value_layer(val_hid).view(-1, 1, self.atom_size)\n",
    "        q_atoms = value + advantage - advantage.mean(dim=1, keepdim=True)\n",
    "        \n",
    "        dist = F.softmax(q_atoms, dim=-1)\n",
    "        dist = dist.clamp(min=1e-3)  # for avoiding nans\n",
    "        \n",
    "        return dist\n",
    "    \n",
    "    def reset_noise(self):\n",
    "        \"\"\"Reset all noisy layers.\"\"\"\n",
    "        self.advantage_hidden_layer.reset_noise()\n",
    "        self.advantage_layer.reset_noise()\n",
    "        self.value_hidden_layer.reset_noise()\n",
    "        self.value_layer.reset_noise()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Rainbow Agent\n",
    "\n",
    "Here is a summary of DQNAgent class.\n",
    "\n",
    "| Method           | Note                                                 |\n",
    "| ---              | ---                                                  |\n",
    "|select_action     | select an action from the input state.               |\n",
    "|step              | take an action and return the response of the env.   |\n",
    "|compute_dqn_loss  | return dqn loss.                                     |\n",
    "|update_model      | update the model by gradient descent.                |\n",
    "|target_hard_update| hard update from the local model to the target model.|\n",
    "|train             | train the agent during num_frames.                   |\n",
    "|test              | test the agent (1 episode).                          |\n",
    "|plot              | plot the training progresses.                        |\n",
    "\n",
    "#### Categorical DQN + Double DQN\n",
    "\n",
    "The idea of Double Q-learning is to reduce overestimations by decomposing the max operation in the target into action selection and action evaluation. Here, we use `self.dqn` instead of `self.dqn_target` to obtain the target actions.\n",
    "\n",
    "```\n",
    "        # Categorical DQN + Double DQN\n",
    "        # target_dqn is used when we don't employ double DQN\n",
    "        next_action = self.dqn(next_state).argmax(1)\n",
    "        next_dist = self.dqn_target.dist(next_state)\n",
    "        next_dist = next_dist[range(self.batch_size), next_action]\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DQNAgent:\n",
    "    \"\"\"DQN Agent interacting with environment.\n",
    "    \n",
    "    Attribute:\n",
    "        env (gym.Env): openAI Gym environment\n",
    "        memory (PrioritizedReplayBuffer): replay memory to store transitions\n",
    "        batch_size (int): batch size for sampling\n",
    "        target_update (int): period for target model's hard update\n",
    "        gamma (float): discount factor\n",
    "        dqn (Network): model to train and select actions\n",
    "        dqn_target (Network): target model to update\n",
    "        optimizer (torch.optim): optimizer for training dqn\n",
    "        transition (list): transition information including \n",
    "                           state, action, reward, next_state, done\n",
    "        v_min (float): min value of support\n",
    "        v_max (float): max value of support\n",
    "        atom_size (int): the unit number of support\n",
    "        support (torch.Tensor): support for categorical dqn\n",
    "        use_n_step (bool): whether to use n_step memory\n",
    "        n_step (int): step number to calculate n-step td error\n",
    "        memory_n (ReplayBuffer): n-step replay buffer\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(\n",
    "        self, \n",
    "        env: gym.Env,\n",
    "        memory_size: int,\n",
    "        batch_size: int,\n",
    "        target_update: int,\n",
    "        seed: int,\n",
    "        gamma: float = 0.99,\n",
    "        # PER parameters\n",
    "        alpha: float = 0.2,\n",
    "        beta: float = 0.6,\n",
    "        prior_eps: float = 1e-6,\n",
    "        # Categorical DQN parameters\n",
    "        v_min: float = 0.0,\n",
    "        v_max: float = 200.0,\n",
    "        atom_size: int = 51,\n",
    "        # N-step Learning\n",
    "        n_step: int = 3,\n",
    "    ):\n",
    "        \"\"\"Initialization.\n",
    "        \n",
    "        Args:\n",
    "            env (gym.Env): openAI Gym environment\n",
    "            memory_size (int): length of memory\n",
    "            batch_size (int): batch size for sampling\n",
    "            target_update (int): period for target model's hard update\n",
    "            lr (float): learning rate\n",
    "            gamma (float): discount factor\n",
    "            alpha (float): determines how much prioritization is used\n",
    "            beta (float): determines how much importance sampling is used\n",
    "            prior_eps (float): guarantees every transition can be sampled\n",
    "            v_min (float): min value of support\n",
    "            v_max (float): max value of support\n",
    "            atom_size (int): the unit number of support\n",
    "            n_step (int): step number to calculate n-step td error\n",
    "        \"\"\"\n",
    "        obs_dim = env.observation_space.shape[0]\n",
    "        action_dim = env.action_space.n\n",
    "        \n",
    "        self.env = env\n",
    "        self.batch_size = batch_size\n",
    "        self.target_update = target_update\n",
    "        self.seed = seed\n",
    "        self.gamma = gamma\n",
    "        # NoisyNet: All attributes related to epsilon are removed\n",
    "        \n",
    "        # device: cpu / gpu\n",
    "        self.device = torch.device(\n",
    "            \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
    "        )\n",
    "        print(self.device)\n",
    "        \n",
    "        # PER\n",
    "        # memory for 1-step Learning\n",
    "        self.beta = beta\n",
    "        self.prior_eps = prior_eps\n",
    "        self.memory = PrioritizedReplayBuffer(\n",
    "            obs_dim, memory_size, batch_size, alpha=alpha, gamma=gamma\n",
    "        )\n",
    "        \n",
    "        # memory for N-step Learning\n",
    "        self.use_n_step = True if n_step > 1 else False\n",
    "        if self.use_n_step:\n",
    "            self.n_step = n_step\n",
    "            self.memory_n = ReplayBuffer(\n",
    "                obs_dim, memory_size, batch_size, n_step=n_step, gamma=gamma\n",
    "            )\n",
    "            \n",
    "        # Categorical DQN parameters\n",
    "        self.v_min = v_min\n",
    "        self.v_max = v_max\n",
    "        self.atom_size = atom_size\n",
    "        self.support = torch.linspace(\n",
    "            self.v_min, self.v_max, self.atom_size\n",
    "        ).to(self.device)\n",
    "\n",
    "        # networks: dqn, dqn_target\n",
    "        self.dqn = Network(\n",
    "            obs_dim, action_dim, self.atom_size, self.support\n",
    "        ).to(self.device)\n",
    "        self.dqn_target = Network(\n",
    "            obs_dim, action_dim, self.atom_size, self.support\n",
    "        ).to(self.device)\n",
    "        self.dqn_target.load_state_dict(self.dqn.state_dict())\n",
    "        self.dqn_target.eval()\n",
    "        \n",
    "        # optimizer\n",
    "        self.optimizer = optim.Adam(self.dqn.parameters())\n",
    "\n",
    "        # transition to store in memory\n",
    "        self.transition = list()\n",
    "        \n",
    "        # mode: train / test\n",
    "        self.is_test = False\n",
    "\n",
    "    def select_action(self, state: np.ndarray) -> np.ndarray:\n",
    "        \"\"\"Select an action from the input state.\"\"\"\n",
    "        # NoisyNet: no epsilon greedy action selection\n",
    "        selected_action = self.dqn(\n",
    "            torch.FloatTensor(state).to(self.device)\n",
    "        ).argmax()\n",
    "        selected_action = selected_action.detach().cpu().numpy()\n",
    "        \n",
    "        if not self.is_test:\n",
    "            self.transition = [state, selected_action]\n",
    "        \n",
    "        return selected_action\n",
    "\n",
    "    def step(self, action: np.ndarray) -> Tuple[np.ndarray, np.float64, bool]:\n",
    "        \"\"\"Take an action and return the response of the env.\"\"\"\n",
    "        next_state, reward, terminated, truncated, _ = self.env.step(action)\n",
    "        done = terminated or truncated\n",
    "        \n",
    "        if not self.is_test:\n",
    "            self.transition += [reward, next_state, done]\n",
    "            \n",
    "            # N-step transition\n",
    "            if self.use_n_step:\n",
    "                one_step_transition = self.memory_n.store(*self.transition)\n",
    "            # 1-step transition\n",
    "            else:\n",
    "                one_step_transition = self.transition\n",
    "\n",
    "            # add a single step transition\n",
    "            if one_step_transition:\n",
    "                self.memory.store(*one_step_transition)\n",
    "    \n",
    "        return next_state, reward, done\n",
    "\n",
    "    def update_model(self) -> torch.Tensor:\n",
    "        \"\"\"Update the model by gradient descent.\"\"\"\n",
    "        # PER needs beta to calculate weights\n",
    "        samples = self.memory.sample_batch(self.beta)\n",
    "        weights = torch.FloatTensor(\n",
    "            samples[\"weights\"].reshape(-1, 1)\n",
    "        ).to(self.device)\n",
    "        indices = samples[\"indices\"]\n",
    "        \n",
    "        # 1-step Learning loss\n",
    "        elementwise_loss = self._compute_dqn_loss(samples, self.gamma)\n",
    "        \n",
    "        # PER: importance sampling before average\n",
    "        loss = torch.mean(elementwise_loss * weights)\n",
    "        \n",
    "        # N-step Learning loss\n",
    "        # we are gonna combine 1-step loss and n-step loss so as to\n",
    "        # prevent high-variance. The original rainbow employs n-step loss only.\n",
    "        if self.use_n_step:\n",
    "            gamma = self.gamma ** self.n_step\n",
    "            samples = self.memory_n.sample_batch_from_idxs(indices)\n",
    "            elementwise_loss_n_loss = self._compute_dqn_loss(samples, gamma)\n",
    "            elementwise_loss += elementwise_loss_n_loss\n",
    "            \n",
    "            # PER: importance sampling before average\n",
    "            loss = torch.mean(elementwise_loss * weights)\n",
    "\n",
    "        self.optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        clip_grad_norm_(self.dqn.parameters(), 100.0)\n",
    "        self.optimizer.step()\n",
    "        \n",
    "        # PER: update priorities\n",
    "        loss_for_prior = elementwise_loss.detach().cpu().numpy()\n",
    "        new_priorities = loss_for_prior + self.prior_eps\n",
    "        self.memory.update_priorities(indices, new_priorities)\n",
    "        \n",
    "        # NoisyNet: reset noise\n",
    "        self.dqn.reset_noise()\n",
    "        self.dqn_target.reset_noise()\n",
    "\n",
    "        return loss.item()\n",
    "        \n",
    "    def train(self, num_frames: int, plotting_interval: int = 200):\n",
    "        \"\"\"Train the agent.\"\"\"\n",
    "        self.is_test = False\n",
    "        \n",
    "        state, _ = self.env.reset(seed=self.seed)\n",
    "        update_cnt = 0\n",
    "        losses = []\n",
    "        scores = []\n",
    "        score = 0\n",
    "\n",
    "        for frame_idx in range(1, num_frames + 1):\n",
    "\n",
    "            action = self.select_action(state)\n",
    "            next_state, reward, done = self.step(action)\n",
    "\n",
    "            state = next_state\n",
    "            score += reward\n",
    "            \n",
    "            # NoisyNet: removed decrease of epsilon\n",
    "            \n",
    "            # PER: increase beta\n",
    "            fraction = min(frame_idx / num_frames, 1.0)\n",
    "            self.beta = self.beta + fraction * (1.0 - self.beta)\n",
    "\n",
    "            # if episode ends\n",
    "            if done:\n",
    "                state, _ = self.env.reset(seed=self.seed)\n",
    "                scores.append(score)\n",
    "                score = 0\n",
    "\n",
    "            # if training is ready\n",
    "            if len(self.memory) >= self.batch_size:\n",
    "                loss = self.update_model()\n",
    "                losses.append(loss)\n",
    "                update_cnt += 1\n",
    "                \n",
    "                # if hard update is needed\n",
    "                if update_cnt % self.target_update == 0:\n",
    "                    self._target_hard_update()\n",
    "\n",
    "            # plotting\n",
    "            if frame_idx % plotting_interval == 0:\n",
    "                self._plot(frame_idx, scores, losses)\n",
    "                \n",
    "        self.env.close()\n",
    "           \n",
    "    def test(self, video_folder: str) -> None:\n",
    "        \"\"\"Test the agent.\"\"\"\n",
    "        self.is_test = True\n",
    "        \n",
    "        # for recording a video\n",
    "        naive_env = self.env\n",
    "        self.env = gym.wrappers.RecordVideo(self.env, video_folder=video_folder)\n",
    "        \n",
    "        state, _ = self.env.reset(seed=self.seed)\n",
    "        done = False\n",
    "        score = 0\n",
    "        frames = []\n",
    "        while not done:\n",
    "            frames.append(self.env.render())\n",
    "            action = self.select_action(state)\n",
    "            next_state, reward, done = self.step(action)\n",
    "\n",
    "            state = next_state\n",
    "            score += reward\n",
    "        \n",
    "        print(\"score: \", score)\n",
    "        self.env.close()\n",
    "        self.display_frames_as_gif(frames)\n",
    "        # reset\n",
    "        self.env = naive_env\n",
    "\n",
    "    def _compute_dqn_loss(self, samples: Dict[str, np.ndarray], gamma: float) -> torch.Tensor:\n",
    "        \"\"\"Return categorical dqn loss.\"\"\"\n",
    "        device = self.device  # for shortening the following lines\n",
    "        state = torch.FloatTensor(samples[\"obs\"]).to(device)\n",
    "        next_state = torch.FloatTensor(samples[\"next_obs\"]).to(device)\n",
    "        action = torch.LongTensor(samples[\"acts\"]).to(device)\n",
    "        reward = torch.FloatTensor(samples[\"rews\"].reshape(-1, 1)).to(device)\n",
    "        done = torch.FloatTensor(samples[\"done\"].reshape(-1, 1)).to(device)\n",
    "        \n",
    "        # Categorical DQN algorithm\n",
    "        delta_z = float(self.v_max - self.v_min) / (self.atom_size - 1)\n",
    "\n",
    "        with torch.no_grad():\n",
    "            # Double DQN\n",
    "            next_action = self.dqn(next_state).argmax(1)\n",
    "            next_dist = self.dqn_target.dist(next_state)\n",
    "            next_dist = next_dist[range(self.batch_size), next_action]\n",
    "\n",
    "            t_z = reward + (1 - done) * gamma * self.support\n",
    "            t_z = t_z.clamp(min=self.v_min, max=self.v_max)\n",
    "            b = (t_z - self.v_min) / delta_z\n",
    "            l = b.floor().long()\n",
    "            u = b.ceil().long()\n",
    "\n",
    "            offset = (\n",
    "                torch.linspace(\n",
    "                    0, (self.batch_size - 1) * self.atom_size, self.batch_size\n",
    "                ).long()\n",
    "                .unsqueeze(1)\n",
    "                .expand(self.batch_size, self.atom_size)\n",
    "                .to(self.device)\n",
    "            )\n",
    "\n",
    "            proj_dist = torch.zeros(next_dist.size(), device=self.device)\n",
    "            proj_dist.view(-1).index_add_(\n",
    "                0, (l + offset).view(-1), (next_dist * (u.float() - b)).view(-1)\n",
    "            )\n",
    "            proj_dist.view(-1).index_add_(\n",
    "                0, (u + offset).view(-1), (next_dist * (b - l.float())).view(-1)\n",
    "            )\n",
    "\n",
    "        dist = self.dqn.dist(state)\n",
    "        log_p = torch.log(dist[range(self.batch_size), action])\n",
    "        elementwise_loss = -(proj_dist * log_p).sum(1)\n",
    "\n",
    "        return elementwise_loss\n",
    "\n",
    "    def _target_hard_update(self):\n",
    "        \"\"\"Hard update: target <- local.\"\"\"\n",
    "        self.dqn_target.load_state_dict(self.dqn.state_dict())\n",
    "                \n",
    "    def _plot(\n",
    "        self, \n",
    "        frame_idx: int, \n",
    "        scores: List[float], \n",
    "        losses: List[float],\n",
    "    ):\n",
    "        \"\"\"Plot the training progresses.\"\"\"\n",
    "        clear_output(True)\n",
    "        plt.figure(figsize=(20, 5))\n",
    "        plt.subplot(131)\n",
    "        plt.title('frame %s. score: %s' % (frame_idx, np.mean(scores[-10:])))\n",
    "        plt.plot(scores)\n",
    "        plt.subplot(132)\n",
    "        plt.title('loss')\n",
    "        plt.plot(losses)\n",
    "        plt.show()\n",
    "        \n",
    "    \n",
    "    def display_frames_as_gif(self, frames):\n",
    "        \"\"\"Displays a list of frames as a gif, with controls.\"\"\"\n",
    "        fig, ax = plt.subplots()\n",
    "\n",
    "        # print(f\"frames[0][0] is {frames[0][0]}\")\n",
    "\n",
    "        print(f\"len {len(frames)}\")\n",
    "\n",
    "        patch = ax.imshow(frames[0])\n",
    "\n",
    "        plt.axis('off')\n",
    "\n",
    "        def animate(i):\n",
    "            patch.set_data(frames[i])\n",
    "\n",
    "        anim = animation.FuncAnimation(\n",
    "            fig, animate, frames=len(frames), interval=50, repeat=False\n",
    "        )\n",
    "\n",
    "        # saving to m4 using ffmpeg writer\n",
    "        writervideo = animation.FFMpegWriter(fps=25)\n",
    "        anim.save('./videos/test_animation.mp4', writer=writervideo)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Environment\n",
    "\n",
    "You can see the [code](https://github.com/Farama-Foundation/Gymnasium/blob/main/gymnasium/envs/classic_control/cartpole.py) and [configurations](https://github.com/Farama-Foundation/Gymnasium/blob/main/gymnasium/envs/classic_control/cartpole.py#L91) of CartPole-v1 from Farama Gymnasium's repository."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# environment\n",
    "env = gym.make(\"CartPole-v1\", render_mode=\"rgb_array\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Set random seed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "seed = 777\n",
    "\n",
    "def seed_torch(seed):\n",
    "    torch.manual_seed(seed)\n",
    "    if torch.backends.cudnn.enabled:\n",
    "        torch.cuda.manual_seed(seed)\n",
    "        torch.backends.cudnn.benchmark = False\n",
    "        torch.backends.cudnn.deterministic = True\n",
    "\n",
    "np.random.seed(seed)\n",
    "random.seed(seed)\n",
    "seed_torch(seed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Initialize"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "cuda\n"
     ]
    }
   ],
   "source": [
    "# parameters\n",
    "num_frames = 100000\n",
    "memory_size = 100000\n",
    "batch_size = 128\n",
    "target_update = 100\n",
    "\n",
    "# train\n",
    "agent = DQNAgent(env, memory_size, batch_size, target_update, seed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAABCkAAAHDCAYAAAAJEUAkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAiKxJREFUeJzt3QeYU2XWwPEzfahDb1KVLkUFRMQOgoiuBeuHimV1dbGyusqKBVFR17Uu1kVwV1nUXXEVK0VQBKQJUqQISO9lhjY933Peyc3cZJJMkkmbyf/nE9Nubl7upNx7ct5zkhwOh0MAAAAAAABiLDnWAwAAAAAAAFAEKQAAAAAAQFwgSAEAAAAAAOICQQoAAAAAABAXCFIAAAAAAIC4QJACAAAAAADEBYIUAAAAAAAgLhCkAAAAAAAAcYEgBQAAAAAAiAsEKRBTCxculNNPP11q1KghSUlJsnTp0lgPCQAAADEyceJEs0/422+/xXooAGKEIAVipqCgQK688krZv3+/vPjii/Kvf/1LWrVqJVXBggUL5I9//KP06NFD0tLSzJetP+PHj5dOnTpJZmamtGvXTl599VWvy23btk2uuuoqqVOnjtSuXVsuueQS2bBhQ9ytE2U99dRT5nXQpUuXMvcVFxfLG2+8ISeddJLUrFlTGjduLIMGDZK5c+eWCerdeeedcuKJJ5rAXsuWLc3fbu3atUHt+Hk77dy5M2z/VgAAACBUqSE/Eqig9evXy6ZNm+Ttt9+W3//+91KVfPHFF/KPf/xDunXrJscff7zfg8g333xTbr/9dhkyZIiMGDFCvv/+e7n77rvl6NGj8uCDD7qWO3z4sJx77rmSnZ0tf/nLX0zwQ4M7Z599tslAqV+/flysE2Vt3bpVnn76aRNY8OaBBx6QF154Qa677joT3Dp48KDZ3vp3+OGHH+TUU081yz377LPmugb39LWlgYW///3vcsopp8j8+fO9BkC8eeKJJ6RNmzZut2mQCgAAAIg5BxAjs2fPduhL8KOPPip32cOHDzsqk507dzqOHj1qLg8fPtz8O73RZerXr+8YPHiw2+1Dhw511KhRw7F//37Xbc8++6xZz4IFC1y3/fLLL46UlBTHyJEj42adlcmxY8ccRUVFEX+eq6++2nHeeec5zj77bMeJJ57odl9BQYGjWrVqjiuuuMLt9g0bNpi/zd133+267YcffnDk5eW5Lbd27VpHRkaG+VuUZ8KECWadCxcurPC/CQCASLC+qzZu3BjroQCIEaZ7ICZuvPFG8yux0l+FNd38nHPOcd2nKe+aaXHhhRdKrVq1ZOjQoeY+/fVel9c094yMDGnRooXcd999cuzYsTLr13Vs3rxZLrroInP5uOOOk3Hjxpn7ly9fLuedd575ZVunmEyaNKnMGPXX7Hvvvdc8hz5X27ZtzS/ZmppfHk3Xr1atWrnLffvtt7Jv3z7z67nd8OHD5ciRI/L555+7bvvPf/4jvXr1MidLx44dpV+/fvLhhx/GzTqDodNFdOpC9erVpW7dutKzZ88yfwudjnLLLbdIs2bNzN9BMwDuuOMOyc/Pdy2j01P0dVGvXj2zrtNOO63MmGbNmmVeZ5MnT5ZRo0aZ14Mum5OTY+7/8ccf5YILLpCsrCxzu5XF4Gn16tXmdRWo7777zmznl156yee0J3396mvGrlGjRpKcnOz2OtL6Lenp6W7L6bQb3Ya//PKLBOPQoUNSVFQU1GMAAIiF1157zXzX6X6A7g/o/ofup9mtW7fOZHs2adLETEtt3ry5XHPNNSZb1DJt2jQ544wzTPag7ht26NDBZJICiC8EKRATf/jDH1xfCjplQOtRPPzww677CwsLZeDAgeZA7fnnnzdfOuqjjz4y0wv0IFUPcHUZPb/hhhvKPIcegOm8fg0yPPfcc9K6dWszn1/n5evBqB4Qa9BBgyD6+I0bN7oeq8+hB6nvvfeeue+VV16Rvn37ysiRI81Uh3D56aefzLmOxU5rWegBqnW/BkZ+/vnnMsspnQqgAR096Iz1OoOh03z0b9+5c2dzAD969GhTk0GDBZbt27ebsWhg4eqrrzZ/h+uvv15mz55t/kZq165d5uD966+/NkEUrf2Qm5srv/vd72TKlCllnnfMmDEmgHH//febKRh60D9z5kw566yzTMDiscceM7frzo8GsrS+iJ3W5PD2evNGX4N33XWXmc7UtWtXr8toEKJ3797mdfn++++bAIj+XTTQpoGb2267ze9zOBwOsw0aNGgggdIpPlp/RIMxup10xw4AgHj0+OOPm6CEBif+9re/mX1CnRI5YMAAE+hX+sOF7hPq1Ef93tUfpfT7U3/EsIIZK1euND9c5eXlmWmPui79DvT2gwSAGItVCgfw7bffep3uMWzYMHP7Qw89VOYx1hQKu7FjxzqSkpIcmzZtKrOOp59+2nXbgQMHTFq9Ljt58mTX7atXrzbLPvbYY67bxowZY6YxaCq9nY5Jp0Js3rw54H+nv+keep+uz5uGDRs6rrnmGnN5z549Zh1PPPFEmeXGjRtn7tN/R6zXGYxLLrmkzNQHTzfccIMjOTnZ6/SE4uJic37vvfeasX7//feu+w4dOuRo06aNo3Xr1q7pHNbr7fjjj3d7Hel62rVr5xg4cKBrnUqX0XWcf/75bs+r69BpG4H4+9//7sjKynLs3r3bXPc23UOtW7fOccopp5h1Wycdp7X9/fnXv/5llh8/fny5y37wwQeOG2+80fHuu+86pkyZ4hg1apSjevXqjgYNGgT1mgYAIBrTPfT7Mz093TFgwAC36Zn6/arLvPPOO+b6Tz/9VO4U4hdffNEso/s/AOIbmRSIW5ot4cme+q7TDPbu3Wt+RddjR2+/5tsLcmpqn6b16RQP7Yhg0dv0PntHC83YOPPMM80v2foc1ql///7m13FN4Q8HTfP3TN+3aKqiNY3FOtc0R2/L2ZeJ5TqDodtcC0pqxwpvNNPjk08+kYsvvthrtofVMUWLlGq2haZvWjSFU39B0fZlq1atcnvcsGHD3F5HWiBUMwn+7//+z0xpsf7W+vrSaS/6t7ZP8dHXmk4dKY+u69FHH5VHHnlEGjZs6HdZzebRNFb9pejjjz82aa2aTXTppZeasfiiU0/0MX369DH/rvLo637ChAkmE0TXrVklmoGiY9UMFAAA4sn06dNNloROv9XMTcutt95qMgKtqZ06VVPpd5qVaenJKhD9v//9L6CpuwBihyAF4lJqaqqZS+hJU+E1DV5rD+iBqB78WbUt7HMOrYNnz4ND/RLT9Xq2BNXbDxw44LquB61fffWVebz9pEEKtXv37rD8O/Vg2V5bwU6nLFgH09a5pih6W86+TCzXGQztCKJ/Qw0waF0FPdi2p1zu2bPHTL8or2OFdojRQJMnnZZh3W/n2dXCmuqgB/mef2/t0KLbx/O1FQite6GvU0079UeDEfq60tegduq47LLLTIBOd8x0ys1f//pXr4/Tzh6DBw82j9OaFykpKRIKDe7odBN9PgAA4on1He75Pa8/nGj3NOt+/W7X6bj6va3TH3Xqh075sH9/67RRnbqrP2BpHSitV6H1twhYAPGHFqSIS/rrvj1irjSD4fzzz5f9+/ebA1wt8KhZEVpYUQMXnl8yvg7afN1ekslfQtelz/XnP//Z67Lt27eXcGjatKn5d2nQQ+tvWDQgoL9u6/xLpQe7uk127NhRZh3WbdaysVxnMDSIsGbNGpk6daoJCP33v/81GQSafaD1KSLFM6BivW40GKA1MbzRYEowNPDx1ltvmVobWlfDHtDR+bOa4aG/AOnfQDM1VqxYYVqQ2mngRreRt7myutOl9VZ0nq0Wkw1l+9tp3Rb9WwAAUFlpjQndH9RMiW+++cbUvRo7dqypU6E/UOn3v37najFwzcDQfY8PPvjA1J/S5UMN9gMIP4IUqDS0I8fatWvl3XffdStcqJWaw+2EE06Qw4cPuzInIsU6KF60aJHpZGLR63rwbN2vARstvKi3e9JCk/prgk4ZiPU6g6VBJv1lQ08a8Lj88svNtAMtUKqZDHogrwfw/mh3Fm8H2DoVwrq/vL+10ucK199bA2e6XXQHSU+e9Befe+65xwQxtOil8tZpQwMammlhp4EOnQKj7wXNftDCoxWlU53Km5ICAEC0Wd/h+j2v+yUW3WfQguee39u6X6MnzWacO3euyZx444035Mknn3Tt++hUTj3pjwNaKFsLt2vgItL7fAACx3QPVBpWhNue8aCXX3755bA/l87dnzdvnpnb6El/vfY8cAyVRu/11/TXX3/d7Xa9rp0XNJ3fcsUVV5j6Dfaggn5pa2cKbb8ZL+vUGgoaIPA1J9SiGRieqZt6wK1/Uz041x0JrZvw2WefeQ2kWK8DDZpoBw79e1m0noRmMmhHl/IO4rVDiQYqtIuMBqY86bSTYFuQ6hQV7SziedK6E9o+Vy9rW1V7Vo52MLFbsmSJ+VucfPLJrts0kKEBHf23at0UrUXhi2bD6Fityufe/i1WTY/FixebjjcAAMQTDRzo/oF297Lv/40fP95kFVr7Hzo91HPfTIMVui9hTWvVTFxP1o8s3qa+AogdMilQaej0Dj2Y1NaR+ku1/vKtUwTstSTC5YEHHpBPP/3UtKrS1EE9kNUDX83m0Pn/mq7vr+WjzpHUtqrKOsC2ovj6q4C20VSaeqjFC7UegwYFdA6lpu9r61PNKNDAgEXba2rbTv1C1m2QlpZmfgXQeZV/+tOfXMvFep1aV0Gna+ivEuecc47PbaStw7SXuf7Koc/3yy+/mMfqWKwMDv2FQ1Mwte6IFsLU6Q968K0H6HPmzDFFsB566CH597//baY/aNaCjkWzbfQXFn19eE4b8qT36xxWfbwGEW666SY57rjjzGtM/w36OtNAiUXHoOPxVzxTXxsaYPGkmRPKfp++tnRqkY5Zd7J0u+i/UVvr6nbXYmEW/Zvo61IzKXRnS7e/3XXXXee6rNko1nbQYI3SIrMa9NBCpFrLQgMh77zzjpnuQZ94AEC80Sw//T7T/QoNpmvLUA3g6/TQXr16ub739McVbTOv+yga/NeAhe6H6Q9cVht7bTuq0z10P0P3xXQKq65Hp4LYi28DiAOxbi+CxOWvBam2//Rm1apVjv79+ztq1qxp2ibeeuutjmXLlpn1aMuq8tbhqwVkq1atHIMHD3a7TdtYjhw50tG2bVvT/kqf7/TTT3c8//zzjvz8/ID+bd5O3tpXvvXWW44OHTqY5znhhBNMmyx7O0zLli1bHFdccYWjdu3aZhtcdNFFpn2lN7Fap7Zy1X+nbgN/3nzzTcdZZ53lqF+/viMjI8Os74EHHnBkZ2e7LaetZbUVqbY61eW0Nae2RM3Ly3Mts379evNvqFOnjiMzM9Nx6qmnOqZOnRrQ682i7csuv/xy13j0NXHVVVc5ZsyYEXIL0kBff9ruVFvBdu7c2bTJ1bal+nfQMXk+3tfryvPj3GrDqy3cLA8//LDjpJNOMutPS0tztGzZ0nHHHXc4du7cGdK/BwCASLYgtbcc7dixo/nuaty4sfnu0tbylg0bNjhuvvlmsy+h+wH16tVznHvuuY7p06e7ltHvc21/3qxZM7Mfo+fXXnttmXbzAGIvSf8X60AJAAAAAAAANSkAAAAAAEBcIEgBAAAAAADiAkEKAAAAAAAQFwhSAAAAAACAuECQAgAAAAAAxAWCFAAAAAAAIC6kSiVUXFws27dvl1q1aklSUlKshwMAQFzQruKHDh2SZs2aSXIyv0NEEvsiAABEZl+kUgYpdKegRYsWsR4GAABxacuWLdK8efNYDyOuFRUVyeOPPy7vvfee7Ny50+xM3XjjjTJq1KiAgg7siwAAEJl9kUoZpNBfLax/eO3atWM9HAAA4kJOTo45cLa+J+Hbs88+K6+//rq8++67cuKJJ8qiRYvkpptukqysLLn77rvLfTz7IgAARGZfpFIGKaxfOHSngB0DAADcMf2gfHPnzpVLLrlEBg8ebK63bt1a/v3vf8uCBQsCejz7IgAARGZfhAmrAAAg4Zx++ukyY8YMWbt2rbm+bNkymTNnjgwaNCjWQwMAIKFVykwKAACAinjooYdMSmrHjh0lJSXF1Kh46qmnZOjQoV6Xz8vLMyeLPhYAAIQfmRQAACDhfPjhh/L+++/LpEmTZMmSJaY2xfPPP2/OvRk7dqypV2GdKJoJAEBkJDm0R0glo79e6A5CdnY280ABAHDi+zFwGmTQbIrhw4e7bnvyySdNt4/Vq1cHlEmh62BbAwAQ3n0RpnsAAICEc/To0TL923XaR3FxsdflMzIyzAkAAEQWQQoAAJBwLr74YlODomXLlqYF6U8//SQvvPCC3HzzzbEeGgAACY0gBQAASDivvvqqPPLII/LHP/5Rdu/eLc2aNZM//OEP8uijj8Z6aAAAJDRqUgAAUEXw/Rg9bGsAACLz/Uh3DwAAAAAAEBcIUgAAAAAAgMoXpHj88cclKSnJ7dSxY0fX/bm5uaaVV/369aVmzZoyZMgQ2bVrl9s6Nm/eLIMHD5bq1atLo0aN5IEHHpDCwsLw/YsAAAAAAEBiFM7UCtjTp08vXUFq6Sruu+8++fzzz+Wjjz4y81DuvPNOufzyy+WHH34w9xcVFZkARZMmTWTu3LmyY8cOueGGGyQtLU2efvrpcP2bAAAAAABAIgQpNCihQQZPWhhj/PjxMmnSJDnvvPPMbRMmTJBOnTrJ/Pnz5bTTTpNvvvlGVq1aZYIcjRs3lpNOOknGjBkjDz74oMnSSE9PD8+/CgAAAAAAVP0gxbp160ybrszMTOnTp4+MHTvW9BhfvHixFBQUSP/+/V3L6lQQvW/evHkmSKHnXbt2NQEKy8CBA+WOO+6QlStXysknnxy+fxniQkFRsSzZdEAKihzSvklNaVQr0+ttgcovLJYlmw9IYVGla0oDREXbRjWlSVamFBYVy8a9R8x1nZr36+5DsjM7L6R1piQnySmt6khGaorsP5Ivq7bnhH3ciaTLcbWlTvV0+WnzATmSVySdmtaS+jUzYj0sxNiRvEL5afNBSU1JktOOrx/r4QAAUDmCFL1795aJEydKhw4dzFSN0aNHy5lnnikrVqyQnTt3mkyIOnXquD1GAxJ6n9Jze4DCut+6z5e8vDxzsrc1QeXw7Jer5R9zNprLdaqnyY9/6SfPfbVGxjtvq2tu6y/pqYGVR3ny81Xyz3mbIjpmoDKrkZ4iCx7uL6/N+lXGfbte3rjuFGlet7pc9OqcCq13yCnN5fkru8nFr86RbQePhW28iej4hjVk5p/OkdGfrZKlWw7KP27oKf07u383IvFsP3hMrhv/o9SrkS5LHjk/1sMBAKByBCkGDRrkutytWzcTtGjVqpV8+OGHUq1aNYkUzdbQgAgqn037j7ouHzxaYH413Gy77YC5rVDSUwOb6mM9tmlWpmRVS4vAiIHKa+2uQ3Ikv0j2HMqTDXuOuN4zxc7Eo2ppKdKqfvWg1nkot9AEJTbvPyKFxQ5XgKJdo5omwwKB0yyy9XuOyBbn51iR8w/DdgQAAKjAdA87zZpo3769/Prrr3L++edLfn6+HDx40C2bQrt7WDUs9HzBggVu67C6f3irc2EZOXKkjBgxwi2TokWLFhUZOqJEU87tHA6HODxmahR53uCHtVP/4AUd5dKTjwvPIIEqoueT02Tv4XzJLSyS3IIic5tOq9KDY3Vyyzoy6dbTglrnNyt3ym3/WmwCFNb7T00Z3ldqZlToKyThaPCo11PTzbZUBCkAAAAq2ILU0+HDh2X9+vXStGlT6dGjh+nSMWPGDNf9a9asMS1HtXaF0vPly5fL7t27XctMmzZNateuLZ07d/b5PBkZGWYZ+wmVg7UzbtGrGqhwu81jGb/rc9aiYKceKEtrRqhj+UVyzBmk0PeM9b5JTQn+Iz/N+Rg9oLa/n1N5DwbN+tzSj0D93CNIAQAAUFZQP4Pdf//9cvHFF5spHtu3b5fHHntMUlJS5NprrzUtR2+55RaT8VCvXj0TSLjrrrtMYEKLZqoBAwaYYMT1118vzz33nKlDMWrUKBk+fLgJRKDqsX7BtWiAotgjSBFUJoVzWXbqgbKqpZcEKXILis1JFRUXuw6G00J431jvNc3IsGdGEaQIXkpSkttnmfV5lmy7HQAAINEFFaTYunWrCUjs27dPGjZsKGeccYZpL6qX1YsvvijJyckyZMgQU+hSO3e89tprrsdrQGPq1Kmmm4cGL2rUqCHDhg2TJ554Ivz/MsQFe3q40muOcpYJZH0EKYCyMtNKsh50qodrukexQwqKi0N+31jBCA122DMpeA8GLyXFFqQodriyyLSbAwAAAEIIUkyePNnv/dqWdNy4cebki2ZhfPHFF8E8LSox/fXVTrMoPGMSzuOngFgHSfyKC5SlhTE9gxSa/WBN97CmbgTDCkbYa1LobdrWFKFnUuhnofV5RiYFAABAmGpSAOUp9IhAOLzUpAiucGbJ+pIJUgBlZDqDFFqPwlWTwlZLIpRf7FO91KQgSBgae/aJPejD9gQAAChFkAIRZf2Ca//1sEx3j6Cme5Scs1MP+A5S2GtSlBTOrPh0D/t6eP+Fxr79KZwJAADgHUEKRLW7h8mk8KhK4VlIM5BMCnbqAX9BCu+ZFGnJFZnuUVqTgvdfaOybzWRSUDgTAACgDIIUiCh7N4DS1nuhZ1K4DpLYqQfKqOYsnHk0v1DyC4vL1KQIbbqHVTjTNj0hhNoWEFPHwwrw2DMpKJwJAABQij1NRL1wpmcmRTBBCqrhA+VnUhw8WuC6rSSTIvRpGqnO7Atdj9VSmOkeobMCrPaaFGRSAAAAlCJIgai3IC3T3SOI6R6l6ea8dAFfQYoDHkEKK1gYSgaEqwVpEYUew8HKpHBrQcr2BAAAcOFID1Ht7lFSODP0TAoOkoBAMinyXbfpdA+rlksoGUjWQXWBvSYFmUwhc033sLUgpcYHAABAKYIUiOp0j5IWpOVnUqzfc9g1p97O2qknPRooK9NZk+KALUih70FXJkVymGpSkMkUMutP4FY4kyAFAACAC3uaiHLhTEeZoITHIrJg437p97fZ8tinK8qsj5oUgG/VrEyKY6XTPTSLorQmRSjTPahJEU7WlBu3wplsTwAAABeCFIhuC1Lnyd90D82iUJv3H/W5PtKjgcALZ1rvsbRQuns432saW7QyMnj/hc7KAqNwJgAAgHcEKRDVIIVmUZRXOPNofpHPWhXWbbQgBfxkUrhN9yi2BReC/8i315/ILSh5b5LJFDor6GO1hbXfBgAAAIIUiCCd2uEKKljF4jRbvJzCmcfyC0uX9eC5PgBla1LY31L6nrGmXVUkk0LlOevE0F0ndNZnV35RScBHUZMCAACgFHuaiErRTOvgyCFlMyms4nFlMim8FNR0zeHml1zA53QPz/ehldEUUuFMW0DCyqRI46A6ZNbmtAI+iqArAABAKYIUiBh7hkSas1ic6e7hUZXCKobpGaTw1vXDKgDITj0QWJBC3zPW1IIU5/uw4pkUvP9CZQV97N2LmO4BAABQiiAFIqbANl8j3Rak8JzG4Tnd46hruodn+9LSLAxqUgC+a1LYaYDCCu6FkgGhUxGst1seNSkqzPoT2DPNKJwJAABQiiAFIsZeGC491ZorX7YFqc/CmX5qV4TSShFIzEyK0ukeoWZApDnfb67Cmbz/QkYmBQAAgH/saSJirGJ9uv9t/VJYdgKHBh/crx9zdffwWJ8tSGHvOADAdyZFSeFMqwVpaB/5VnDDmu7BQXXorCKZFM4EAADwjiAFIqa0WF+yq1ict0wKz4yJIz6me9gzKZjuAfju7mFX0oLUGVwIMbhnBSWoSVFxVpzIyqRgWwIAALgjSIGIsX691QMj/c9VONPhfuDjGYxwZVL4CWawYw+UlZnuqyZF6N097JlLrukeZDKFzGrfSpACAADAO4IUiHjhTD0wsvbDS4pfus+PL/LV3cMzk8JW44J0c6CszFT/NSlCrSVhPY6aFBVnxXfyrY4rZIUBAAC4YU8TEWMFH3QefJJzR1xvskIN1vx4z4wJXy1I7TUpmMMNlJWWklTml/mSFqThne5BkDB8hTPZlgAAAO4IUiBirHnwetCUZMuksGIP1sFUsY8WpJ7BCytowU494J0GAzOdnXTcpntYU6+SK1g4s4ApChVl/QmsIAUBVwAAAHcEKRAx9o4CyfZMCmewQX/19ZtJ4aO7BwdIgG/VPOpSmEyK4gpmUlg1KQqpSVFRKR7dPfg8gyfrOxIAgERFkAIRYz8wsnbDHaI1KcTtV117JoVOEbFSyot81KQgkwLwLcOjLoW9cKYVGAyW9Z6jJkXFUTgTAADAP/Y0ETGlKeZaOLO0u4e/wpnWVA9ze5maFKRHA8FnUpRO97AOkINlBSVoQRrGwpnWtqRwJgAAgBuCFIgYe0eB0poUpS1IS6d7lG0/6q1WBTUpgPJlppX9WM+zpmmE2oLUoyYF78EwZFLYavYAAACgFEEKRLxwppnu4apJoYUz3TMp7MEIqx6F90yKiv0aDCSCamll25DmOoMLVkeditakSKEmRcisP0F+ITV2AAAAvOFoD5Gf7qEtSJ23mSCFnxakR2zTPTwzKUpT1iM8cKASy/QSpDjmrCUR6gGxqwWpFewgUBiGwplkUgAAAHjDniaiMN0jydV2T2/xV5PCbbqHR4Hz0ukevGyBQAtn2t9joRfOtGpS0JEifIUz2ZYAAADecLSHyHf3cCucqdM9xJVh4Xe6h2cmBS1IgaALZ9pZ77lgUZMifCicCQAA4B9BCkSMNT1Dp3W4WpCa7h4ll9OsTApHYDUprKAFQQrAt8xU3x/roQYXqEkRPhTOBAAA8I8gBaJcOFP/769wpu+aFAQpgApmUlSwJkWBFXhkylXIrGSWAgpnAgAAeMWeJiLGCiroAU5pC1JHaSaFl8KZgWRSkGoOBFY4s4ZHwCL06R7uj+PAOnSuqTPOIG4y2zJmWrdubQLonqfhw4fHemgAACS01FgPAFVXgSuokOyqSaE3lS2cKV4LZ+piGtSwsjCoSQEEHqRIT0mW9NRkOWJ7T1U0k8J1nekeFe/u4axJQdA1dhYuXChFRaXvjxUrVsj5558vV155ZUzGQ3kSAABKEKRAxBTap3s4b7MXzrQ6DVhBC89MipL7SgvNFTkLcRKkAHzLTCvJeshISy6TORFqcMHzcXTYCZ1VKNPV3YMj05hp2LCh2/VnnnlGTjjhBDn77LNjNiYAAMB0D0RQadvD0kyK8lqQ2mtSeN5nZVwQpAB8q+bMpNBzqzitJdRaEmUyKXgPhozCmfEpPz9f3nvvPbn55ptd2XsAACA2yKRAxFhF9sxOuHOfzwQoPFqQugcpPDMp7EEK0qOBQKd76LnDerM5U8lDrX9ATYrwoXBmfPrkk0/k4MGDcuONN/pcJi8vz5wsOTk5URodAACJhUwKRHy6h07rsPbDS1qQuhfA9Dfdwx7AsGpSWFkZAMrLpCj9iK9IRw5qUoSPFSgikyK+jB8/XgYNGiTNmjXzuczYsWMlKyvLdWrRokVUxwgAQKIgSIEoF84s/W3Xmtfud7qHWyaFc30cIAHltiDVc/t7pSLvG2pShE+qR+FMghSxt2nTJpk+fbr8/ve/97vcyJEjJTs723XasmVL1MYIAEAiYU8TEeOanqGFM2374VbmRECFM91qUljp0bxsAV/6nFBf+nVsJDf1be32XqnIwbBnJgUH1uEonOlsQUpmWMxNmDBBGjVqJIMHD/a7XEZGhtSuXdvtBAAAwo+jPURMobMmhR7guGVSOOMO3gpn2luQet7nakHKPj3gU+3MNBl/Yy+55KTjXIFAq4BtqDwDg9SFCV/hTLZlbBUXF5sgxbBhwyQ1lTJdAADEA4IUiHjhTHsbRE2uKG1Bak33KH3M0QL36R62GIUrq4JMCiAw9gPgihwMe073SCFSGDLPWBFZKbGl0zw2b95sunoAAID4wM8GiJhC53QPbYPorwWp23SPPN/dPaxMCn55BAJjrx1RoSBFmFqZgk4p8WbAgAHisH3PAACA2GNPExHjmp6RnOyqSeFWONP5a6y/FqRF3mpS8CsuEBD3wpnh6+7BgXXoyKQAAADwjyAFIt6CVA+UXMXhvLQgtXfwKNPdw2tNCnbqgUDYAxMV6e5RpiYFgcKQeRbKpHAmAACAO4IUiHjhTC3eZ/1YaC+caaWi2zt4HCvwPd3DWo7pHkCMa1LwHgyZ59+BzzMAAAB3BCkQMQWuoIK+zKz6E6X3W50HrGyJgqJiV7FNv5kU7NQDIQQpwjfdg5oUofP8/Erm8wwAAMANe5qImKJi+3QP5222zAgrhdzKlsgrLG3zkZnmfp99fQQpgFBqUlRkugeZFOFCO1cAAAD/CFIg8i1I7YUzbZkRnpkUebapHplpKSXL2xIrrFalHCABse3uQU2K0FE4EwAAwD+CFIhq4UxryoZ959ya4ZHvXF6DF66imm7dPZzrY6ceiG53D4/H8h4MHYUzAQAA/CNIgYixAhIadPCWSWEd+Fi35RWUBCEyUlNcO+7ealIwhxuIcuHMMsUe+eoIlWcWClkpAAAA7tjTRMS7e+gc7CQr6GCrMeGZLWFlUqSnJruyLNxrUtDdAwi9BWly+GpScGAdMjIpAAAA/CNIgYgpdE7PSEtOcvb2cM+McAUpHJ6ZFMleMymsy56F5wB4p+89b5cr/Os/gcKQeWahsC3hyb3HFQAAiSc11gNAAhTOTCkNOrhP93C/Lb+oyJVJYSVQ2DMprOke7NQDgbEH9CpSoJED6/DxTGhh+hoAAIA7fpJGxLimZ/hoQWod+HjLpHAV1Swuuz526oHAWB10Si5XoHAmNSnCxnN6RwrTPQAAANywp4mIKbC6e+h0Dy+ZFNYBlKtwpq0mhXVM5FaTwnmZX3GBULp7hP6+oSZF+FA4EwAAwD+CFIiY0ukZWjizbCZFio9MinQf00OKXIU42akHojrdg5oUYUPhTAAAAP8IUiBiCp2ZEaYFqbN0phW4sB/4WFM6rO4e2oLUNd3DS00KghRAKIUzKzLdg5oU4cK2BAAA8I8gBSLGHlRwTd/w0t3DNd2joLRwprfuHtbUD3bqgVBakFakcKbHdA/egyHzjBVRYwcAAMAdQQpETKFzeoYW7HNN97AVwvQsnFmaSVFaONNbdw8OkIDggwsVCe7Z33MpthozCJ5noUxKUgAAALgjSIGIKSwutnX3KBt0cAUirBakhbbCmV67e5QW4gQQbOHM5LCshyBhxXhmtKRU4O8CAABQFVVo7+iZZ54xv6jde++9rttyc3Nl+PDhUr9+falZs6YMGTJEdu3a5fa4zZs3y+DBg6V69erSqFEjeeCBB6SwsLAiQ0HcF850n76hxznWvrmrcGahrSaFl+4eVmYG6dFAtKd7JHutc4Hg0YIUAAAgQkGKhQsXyptvvindunVzu/2+++6Tzz77TD766COZPXu2bN++XS6//HLX/UVFRSZAkZ+fL3PnzpV3331XJk6cKI8++mioQ0GcsoIKJS1I3QMSGrTwrDthz6TwzLIwl6lJAcTFdA+EjsKZAAAAEQhSHD58WIYOHSpvv/221K1b13V7dna2jB8/Xl544QU577zzpEePHjJhwgQTjJg/f75Z5ptvvpFVq1bJe++9JyeddJIMGjRIxowZI+PGjTOBC1QdBUX26R7iFnQoyaTwKJxZWOSqSeHKvPBak4L0aCD4IEVyzKeNgMKZAAAA5Qlpb1Onc2g2RP/+/d1uX7x4sRQUFLjd3rFjR2nZsqXMmzfPXNfzrl27SuPGjV3LDBw4UHJycmTlypVeny8vL8/cbz8h/llBBVM4U9yzJvS6K5PCKpzpmu6R7EqBtnf3sC5zjAQERt97pZfD092DTIqK8dx+fJ4BAAC4S5UgTZ48WZYsWWKme3jauXOnpKenS506ddxu14CE3mctYw9QWPdb93kzduxYGT16dLBDRQw5HA5XUEEPcJLLTPco3Vm3imOW1qTw3t2jNEjBXj0Q/DSNCmRSUJMibMq2c+XzDAAAwC6ovaMtW7bIPffcI++//75kZmZKtIwcOdJMJbFOOg5UjiwKX4Uz7UEKKxBRXneP0kKcHCQBgbBnT1Qkk8It2EHPzAqhcCYAAEAYgxQ6nWP37t1yyimnSGpqqjlpccxXXnnFXNaMCK0rcfDgQbfHaXePJk2amMt67tntw7puLeMpIyNDateu7XZC5Siaac1ndxXOdNWkKFs4M89eONOjhoXnYwEElwFRkeCeW00KfvmvEM/tx/QZAAAAd0Htbfbr10+WL18uS5cudZ169uxpimhal9PS0mTGjBmux6xZs8a0HO3Tp4+5rue6Dg12WKZNm2YCD507dw5mOIhjBcXF7kEKcc+a0GuehTNLa1KkeJ3uQSYFEBx71kNKBYofhCvYgbKFMwlSAAAAVKAmRa1ataRLly5ut9WoUUPq16/vuv2WW26RESNGSL169Uzg4a677jKBidNOO83cP2DAABOMuP766+W5554zdShGjRplinFqxgSqhiJbJkVacnJpTQpbNoSrOKbDvbuHme7hpbuHFcwg3RwIjL73Si9TODMeUDgTAAAgzIUzy/Piiy9KcnKyDBkyxHTl0M4dr732muv+lJQUmTp1qtxxxx0meKFBjmHDhskTTzwR7qEgDjIpNNag9SU8a0yU3C5ep3tk2IIU9ukeZFIAsWkdag8M2teJcAQpiFIAAACENUgxa9Yst+taUHPcuHHm5EurVq3kiy++qOhToxLUpLB+ybV2y13TPTSTwk/hzNLOH/aaFCX3U2gOCIw9oFehmhRh6hKCsp9ffJ4BAAC4Y28TEQ1SWL+6enb30GMe13SPMpkUKaWZF6UxClsLUnbqgUDYsycqkgFBC9LwoXAmAACAfwQpEBGFVtaDcwfc+rHQnklhBSI09uBwONwzKfx09yDdHAglk6IihTOpSREuFM6Eb7wWAABQBCkQEVb9iDTnL7llC2e6pznrzVbhTFOTwpVJUbYmBS1IgVBah4b+vtH3o/W2I0hYMRTOBAAA8I/dI0REgbNCpnVgZLUgtQINeosViLCCF/lF9kyKsi1IrayKivwiDCQSt9ahFQwuWO9lalJUDIUzAQAA/GPvCBFR5COTotieSWHbWddgRF5BaXcPV1FNL909SI8GApNmC0xY78WKBjyoSVExFM4EAADwjyAFIqLAWfGytCaF+/QNnbJh3zm3Z1JokKK00GbpOimcCQTH/l6p6PumNJOC9194MynYngAAAHYEKRARhdZ0D1d3D3HLjNDr9iznIlsmRXpKimuetreaFOzUA4GxZ0/YsypCkeJ8PDUpKsYUDbZtQj7PAAAA3BGkQGQLZzojEcnlZFIU2zMp0mw1KYq91aRgpx6IZncP+7qooRDuDJeYDgVx6ODRglgPAQCAmGL3CBENUrgyKZy326dv2HfUNUBhTedIT/Hf3YNfHoHA2AMTFZ/uQU2KyAQp+BpGiUO5pcGJ/UfyYzoWAABiib0jRHa6h3Nn3Ao6uApnJpekPVvJFLn5pdELX5kU1KQAgpMaxsKZ1vuO91/F2bPIKJwJz+84VVhsi+gDAJBgCFIgooUzUz0OjKzMCKslqbWDfrSg0LWMZlK4unvYMimsxzLdA4hB4UxqUkTm78L2hJNbvKr0qw8AgIRDkAIR4Zn1YNWksLcgNefOC8fyi1zLp9qnezh/THI4HGRSALEsnEkmRWSCFGRSwKX0tUCMAgCQyAhSICKsVFUr68HaD7fqSlgtRq0ddCtIoVkU9tutTAp7GiwHSUBg9L1ivfc8s5qCZRXBrWgBTrh/hrE54Y0tiRAAgITD7hEiwgoulGZSiFuwwTpwsu4/VlDkqkfhbXkruGF/DIDytalfQ2plpkq96ukVWo/1vmO6VcXZP8MI+sBiT6pxkEsBAEhgqbEeAKoma5qGNc3DNd3D1oK05LxkuaMemRSe3T3stSnYqQcCN2V4X8kvLJZq6SkVWo9Vi4IaChVH4Ux4wysBAIASBCkQEVbtCc+sB1cmhfjPpPDs7mHPpCBGAQQuq1paWNZjZVBY0z4QOnugh6APvGG6BwAgkbG3iYiwMiDKy6RwBSl8ZVI4gxNFzm4hikwKIPqs9x3TrSqOTIr4sW3bNrnuuuukfv36Uq1aNenatassWrQoJmOxvhfV8m3ZMRkDAADxgEwKRERpJ46S60k+alJYO2VWJkV6aklKemkLUufjbD8rcYwERB81KcLHCsIqgj6xc+DAAenbt6+ce+658uWXX0rDhg1l3bp1Urdu3VgPTdbvORzrIQAAEDMEKRClwpnumRGu7h7O+62aFBmp/rt76AGS9VgA0UNNivCxB3oIUsTOs88+Ky1atJAJEya4bmvTpo3Eg5emrZM/ntM21sMAACAmyJtHRFhBBSs4YcUVrMwIa7fcuj/XlUnhfbqHVZPC/gskgOihJkVk0vr5SIudTz/9VHr27ClXXnmlNGrUSE4++WR5++23JR7kW9WnAQBIQOxtIsLTPawghXvQwTrO8axJYWVSuFqQWt09bJkUAKIvhZoU4c9KITMspjZs2CCvv/66tGvXTr7++mu544475O6775Z3333X6/J5eXmSk5PjdgonamUCAFCC6R6I7HQPj1ajnhkWPqd7WDUpPDIpOEACYiM91ZlJ4XyPInTW5yJFM2OruLjYZFI8/fTT5rpmUqxYsULeeOMNGTZsWJnlx44dK6NHj47YeBy09AAAwGBvExFhZapa0zOSnBM8rMyI0uke4jbdI8NZONOzhkVRcckKCVIAsXFNr5ZyToeG0q9jo1gPpdKzPsf4PIutpk2bSufOnd1u69Spk2zevNnr8iNHjpTs7GzXacuWLWEdDyEKAABKkEmBiHAFFXxkUpQtnFnoVpOitLuHeyYF0z2A2DirfUNzQsURpIgP2tljzZo1bretXbtWWrVq5XX5jIwMc4oUMikAAChBJgWik0nhsS/uqwVp2e4e1vqY7gGgarA+9/g4i6377rtP5s+fb6Z7/PrrrzJp0iR56623ZPjw4TEZDzEKAABKEKRARFjTOqzMB8/icJ41KazCmb66e5S2IOUlC6BqFM5MTeHzLJZ69eolU6ZMkX//+9/SpUsXGTNmjLz00ksydOjQmIyHGAUAACWY7oGIKPbs7uFxv3XdFaTwzKRw7rt7TvcgRgGg6mRSkEoRaxdddJE5xdP3JgAAiY5DPkQ0k8LXzrjn7Uc9Myk8CmeWtiDlJQugcrMyzKixAztCFAAAlOCIDxHOpCi57hlbsGIWViaF1d0jPcV7dw9akAKoKiicCW+oSQEAQAmCFIgIK7jg2YK0TJDCI5MiI817dw9X4UzSowFUcq5MMr6BYeMglwIAAINdJER0uocVVPCMLXjupFs1KdKdqReemRR09wBQ5QpnEqWADZkUAACUYA8J0Smc6RGl8JzuYe2cWTUpSjMpxL27h3PnHgAqK1qQwhuCFAAAlCBIgSgVznS/31dBzRoZKf67ezDdA0CVKZzJVzBKMd0DAIAS7CEhIoqKxaMFqffgguf0jRrpqT6me5SskGr4ACo7q1aPdQ4oOpACAFCCIAWiMt3DVyaFZyHMmpm+ghQl91OTAkBlZ33uWRljgHIw3wMAAINdJERluofnLA3ruucviTUzUr129yi0MimoSQGgkrM+x1KY7gEbQhQAAJRgDwkRzqQQr4UzfWVS1HAGKXx196AmBYDKrvTzL9YjQTwhkwIAgBIEKRClwpmeQQrv0zfKZlKU3F7gnO9htSgFgMqKwpnwhhgFAAAl2ENCRFiZD6WFMz15LxxXGqQQt+ke+YXFbi1KAaDyF86M9UgQTwhSAABQgl0kRIQVXHAVzvR4pbkyKWwxCk22qJ6e4nW6Rx5BCgBVrnAm8z1Q9nsTAIBExxEfIsKzhoRnC1JvhTNrpKe6ale4pnt4BCkyCFIAqORSKJwJL9o1rhXrIQAAEBfYQ0JEeLYM9ax36a1wZo2MlDL3W7UtmO4BoMplUpBIAZs2DWrEeggAAMQFjvgQ2ekerhak3jMp7OnOVj0K9+keJdfzXYUzSwMZAFCZC2eSSQEAAFAWe0iI7HQPqyaFxy+GVtAi2UeQorS7B5kUAKoW63OPZkUAAABlsYuECBfOFB8tSL1N97AHKcSjcGaROSdIAaCyo3AmAACAbxzxIUqFM91Z1+076fYghfU4z0wKCmcCqOwonAkAAOAbe0iIaJCitHCmZyaFdV56ey1v0z2c6yFIAaCqSHemiqWRSQEAAFBG6VEhENHCme73l7YaFb+ZFK7uHlbhTIIUACq58zs3lu/X7ZUre7aI9VAAAADiDkEKRKlwpvfuHvbCmTW8ZlKIe+FMKs0BqORa1a8h7958aqyHAQAAEJc44kNEFJXEKHxnUkjZwpk1M1J8ZlLk0d0DAFDFdWueFeshAAAQcxzxISKsWhJWRoTn1GvreoqPFqTJZbp7WDUpSgMZAABUJQVWhB8AgARGkAJRme7h2d/DNd3DVwtS2+0a8HBN9yCTAgBQRW3ed8R12eHMJAQAINFwxIeoFM4sm0mR5DeTwn67rosgBQCgqjuSX+S6fNR2GQCARMIRHyKcSSE+CmeWDVK4dfew3a51KVzdPSicCQBIABv3lmZVAACQSDjiQ0QUlduCtGzwomamr+ketu4eZFIAABIAsz0AAImKIz5EqXBmko/CmVLudA8NeOQVlqS9ZhCkAABUUQNPbOy6/MP6vTEdCwAAscIRHyKaSWGftuGtBamvwpn2mIZOHbEyKQhSAACqqno1MlyXa2emxXQsAADECkd8iAidouFeONN7JoVVYFPVTKe7BwAgcdm/Kh3CfA8AQGLiiA8RLZxpTdsoW5Oi5IZj+cW2TIoU3909rMKZBCkAAFWU/avS+TUKAEDC4YgPEVFodffwkUlhXT1WUNpiLdVWoEKDGNYyuq6CopL10d0DAJAQmRRUzgQAJKigjvhef/116datm9SuXduc+vTpI19++aXr/tzcXBk+fLjUr19fatasKUOGDJFdu3a5rWPz5s0yePBgqV69ujRq1EgeeOABKSwsDN+/CHHBmsZRWjjT/X4raJFrC1J4SvGyDJkUAICq6uz2jcoUoAYAINEEdcTXvHlzeeaZZ2Tx4sWyaNEiOe+88+SSSy6RlStXmvvvu+8++eyzz+Sjjz6S2bNny/bt2+Xyyy93Pb6oqMgEKPLz82Xu3Lny7rvvysSJE+XRRx8N/78M8T3dw3l+NN93gMoqumnPtshILZ0SAgBAVXJcnWquy8QoAACJqrRSYQAuvvhit+tPPfWUya6YP3++CWCMHz9eJk2aZIIXasKECdKpUydz/2mnnSbffPONrFq1SqZPny6NGzeWk046ScaMGSMPPvigPP7445Kenh7efx3ipgWpVYOibACitCaFr0yKY/mlQYq0FO/dQgAAqOySbT8d2QtLAwCQSELOndesiMmTJ8uRI0fMtA/NrigoKJD+/fu7lunYsaO0bNlS5s2bZ67redeuXU2AwjJw4EDJyclxZWOgarUgtQINnqEF67qPDqVu91mZFDrVwzPYAQBAVWGv37TvSH5MxwIAQKXIpFDLly83QQmtP6F1J6ZMmSKdO3eWpUuXmkyIOnXquC2vAYmdO3eay3puD1BY91v3+ZKXl2dOFg1qoHJM97B+FSpbOLPk+v0DOsjybdlyU982ZdZhZVtYNSkyKJoJAKjC7IH7VdvZ1wEAJKaggxQdOnQwAYns7Gz5z3/+I8OGDTP1JyJp7NixMnr06Ig+ByJbOLNsC9KS8xb1qsvMP53jdR3WY602pRTNBABUZfZsQaZ7AAASVdBHfZot0bZtW+nRo4cJHnTv3l1efvlladKkiSmIefDgQbfltbuH3qf03LPbh3XdWsabkSNHmqCIddqyZUuww0asCmf6aEHqb5pHmZoUtukeAABUVZ7flQAAJKIKH/UVFxebqRgatEhLS5MZM2a47luzZo1pOarTQ5Se63SR3bt3u5aZNm2aaWeqU0Z8ycjIcLU9tU6IX9rb3apKbk3Z8JRUpkpF+d09MghSAACqMPtXJpkUAIBEFdR0D81oGDRokCmGeejQIdPJY9asWfL1119LVlaW3HLLLTJixAipV6+eCSTcddddJjChnT3UgAEDTDDi+uuvl+eee87UoRg1apQMHz7cBCJQNdjbprkyKZJDz6TIdXb3IJMCAFCVVU8v3S2rlhb0jFwAAKqEoL4BNQPihhtukB07dpigRLdu3UyA4vzzzzf3v/jii5KcnCxDhgwx2RXaueO1115zPT4lJUWmTp0qd9xxhwle1KhRw9S0eOKJJ8L/L0PMp3rYgxOeQYlAunS4alIw3QMAkAAa1ir9waZ945oxHQsAAJUiSDF+/Hi/92dmZsq4cePMyZdWrVrJF198EczTopKxp6i6Cmd6TO8IZNqt1RnkqJVJQXcPAECC+GUH3T0AAImJoz5ENJOitHBm8MXBrGWsFqRkUgAAEsW3a/bEeggAAMQER30IuyJbJoWVDeFZJzOQ+uWu7h7OTIqM1JTwDRIAkNAef/xxM/XQfurYsWOshwUAQMKjKhPCrthrJoVH4cwAKmday+QWkkkBAAi/E088UaZPn+66nprKbhEAALHGtzEiO93DVZMieJ6ZFAQpAADhpEGJJk2axHoYAADAhqM+RGy6h8YYknxlUgRSk8Kju0cGhTMBAGG0bt06adasmRx//PEydOhQ2bx5c6yHBABAwiOTAmFXXOyeCeE9SFH+eqyYBJkUAIBw6927t0ycOFE6dOhgWquPHj1azjzzTFmxYoXUqlWrzPLaWl1Plpwcum8AABAJBCkQsUwKt7oTnoUzk4KY7mFlUhCkAACEyaBBg1yXu3XrZoIW2ib9ww8/lFtuuaXM8mPHjjWBDAAAEFkc9SFihTPdMykk6OkeSR5BCjIpAACRUqdOHWnfvr38+uuvXu8fOXKkZGdnu05btmyJ+Jgctm5ZAAAkCo76ELHCmVbRTHvAIRjW43OZ7gEAiLDDhw/L+vXrpWnTpl7vz8jIkNq1a7udIu0vU1ZE/DkAAIg3HPUhctM9bHGJUDIprEyM3MKSIhfpKSlhHScAIHHdf//9Mnv2bPntt99k7ty5ctlll0lKSopce+21Ei/+vYBCngCAxENNCkRuuoc9k8KjKEUgiRXJzhDa0fxCc04mBQAgXLZu3WoCEvv27ZOGDRvKGWecIfPnzzeX40luQZFkphGkBwAkDoIUiFgmhft0jxAyKazpHgXOTAqCFACAMJk8ebJUBos3HZC+bRvEehgAAEQNR32IWE2K5Aq2IPV8DN09AACJZvycjbEeAgAAUcVRH8KuuCTxwW8mRSDzPeyPV2RSAAASzczVu2M9BAAAooqjPkSwcCaZFAAAVNT2g8diPQQAAKKGoz5EpwWpxzKehTS9yaqW5nY9PYWXKwAg8Vz91rxYDwEAgKjhqA9hVxxQ4czy19O2UU2360z3AAAkoi37yaQAACQOjvoQwcKZpbclJQXfgrR941pu1zNSacEGAAAAAFUZQQqEXbGX6R7lBS28aUcmBQAAAAAkFI76EJXCmZ6BiQASKaRFvepuxTIJUgAAAABA1cZRH6JSOFPZr3oGMLzRx9vrUlA4EwCQqD5atCXWQwAAICo46kNUCmd6ZlIkB/jKs0/5IJMCAJCoHvjPz7EeAgAAUcFRH8KuqFi8T/dwuxzIhA+RdrbimfapHwAAAACAqoejPkRxuofvlqSBZFIQpAAAAACAqo2jPkRuuoeftqOBdPdQJ9iCFGnUpAAAIGC/7T0in/y0zdV1CwCAyiA11gNA1VPo3BnyrDthz6TwSLLwqXX9GtK+cU3RVdaulhbWcQIAUBUdySuUn7dmy7VvzzfX7/1gqUy96wzpclxWrIcGAEC5CFIg7KxfbMoUznS7HFiUQtfx5T1nmewMz/UBAAB3uQVFcuJjX5e5/aJX58ifL+ggfzynbUzGBQBAoMifRwRrUri/vOwzPIKJN2hwgqkeAIBE9+XyHX7v/2nzAen4yFc+73/uqzURGBUAAOHFkR/CrshVk8L99mRbZCLQmhQAAKDEHe8v8Xv/Za/NLXcd+YXOFlwAAMQpghSIzXQPYhQAAISNw/kDQXnaj/pS8gqLIj4eAABCRZACEcuksBfK9LzueR8AAAjd1yt3Brxs32dmEqgAAMQtghSIXiaFvQVptAcFAEAVVFBUbIpl3v6e/6kgdnsP58vL09dFdFwAAISKIAUiVjjTXoPCsw6FZ3tSAAAg8rvuzYJa/py/zvJbLNOX12atD/oxAABEA4eKCLsi57TYlKSKtyAFACCRPH9ld7m6Zwuf92/Yc9jt+raDx6IwKgAAoocgBaI23cNeh4KSFAAAlJWemizPXtFNfn1qkNf7z/vb7KgX2wQAIJoIUiBqhTPdalIQpQAAwKfUFN+7aIfzCs251qKoiIEvfVehxwMAEAkEKRCxmhSe+1fu3T2iPSoAACqXG09v7fX2R/+3wpyHUovCbu2uw3L9+B8rtA4AAMKNIAVi0t2DFqQAAPj3+O9O9Hr7x0u2he05vl+3N2zrAgAgHAhSIDbTPaI9KAAAqpDWD30etnW9OoN2pACA+EGQAjEqnEmYAgCA8vRuUy/kx8558FxZPKq/zH7gHKmWluJzub9NWxvycwAAEG4EKRC9TAr7ZWIUAACU64Y+3utSlOfRizpL87rVpX7NDGlVv4bM/0s/v8tv3HskxBECABBeBCkQdkXFUm4mBTUpAAAoX0Zq8LtqL17dXW4+o43bbVnV0vw+5tznZwX9PAAARAJBCoRdsTOTwjNIYU+lIEYBAED50kIIUlx2cnOvtw/q0iQMIwIAILIIUiBiLUg9syVoQQoAQHD6nlA/bOt6/boefmtcFFqpkAAAxBBBCkQsSJHi8eqyByYonAkAQPlSU5Ll1CCKZw7r08rv/ZNuPc3nfb2emh7U2AAAiASCFIjcdI8yhTNt3T2iPioAACqna3q1CHjZxy4+0e/9OhXz7vPaer3vwNEC+WDhZtl/JD/oMQIAEC4EKRC56R4eczrsMQsKZwIAEJgux2UFtNwNfVqV+e71Rrt9+PLgf5fLdf/4MajxAQAQTgQpEL1MCtt1YhQAAASmfo30cpf5Xfdm8sQlXQJaX3I5e3+rduQEOjQAAMKOIAWilklhv0omBQAAgalfM6PcZZ67olvA6xvUpam0a1TT7zJb9h8NeH0AAIQTQQqEnVUc3LMFKXEJAAAiI92zWrUfmWkp8s19Z/ld5sznvpXdOblhGBkAAMEhSIGoTfdwb0FKxAIAgHC4t3+7gGpReE7B/P7P5/pd5vlv1lRwZAAABI8gBaJXONN+mRgFAAAB+/0ZbXzed2//9iGts0W96vLeLb193v/hoq2yYc/hkNYNAECoCFIg7IpcmRTis3AmmRQAAARuxIDQAhHl6X18Pb/3bzt4LCLPCwCALwQpEHbFzkyKFL+FM6M9KgAAKq/q6akRWW9aSrLfLI3Rn62SyQs2R+S5AQDwhiAFojfdgxakAACE7H4v2RSDuzat8HpHXdTZ532/7j4sD328XNYz7QMAECUEKRDFwpneAxYAAKB8p7apX+a2V649OSzrHtC5sd/7+/1ttmQfKwjLcwEA4A9BCkSxcKYtkyLqowIAoHI7tU09ufbUFpLq/H5tUa9amamVoXpoUMdyl9lOfQoAQBQQpEDYFZXEKMpkUtivUjgTABAvnnnmGZPhd++990q8G3t5N/llzAXy+tBT5JM/9g3beo9vWLPcZb5ftydszwcAgC8EKRC1wpn2uAQxCgBAPFi4cKG8+eab0q1bN6kstNjloK5NpX7NjLCu9/azT/B7/9NfrA7r8wEA4A1BCkRtuoc9e4JMCgBArB0+fFiGDh0qb7/9ttStW1cSnU75WPCXfn6X+XnrwaiNBwCQmAhSIOyKfBbOpLsHACB+DB8+XAYPHiz9+/eP9VDiRqPamX7v/93ff5CCouKojQcAkHiCClKMHTtWevXqJbVq1ZJGjRrJpZdeKmvWrHFbJjc313zp169fX2rWrClDhgyRXbt2uS2zefNms1NQvXp1s54HHnhACgsLw/MvQhxN9xA/0z2IUgAAYmfy5MmyZMkSs28TiLy8PMnJyXE7JaoV27JjPQQAQBUWVJBi9uzZJgAxf/58mTZtmhQUFMiAAQPkyJEjrmXuu+8++eyzz+Sjjz4yy2/fvl0uv/xy1/1FRUUmQJGfny9z586Vd999VyZOnCiPPvpoeP9liJlCa7pHmcKZ9ukeUR8WAADGli1b5J577pH3339fMjP9Zw5YNJiRlZXlOrVo0UKqqhWjB/q9/7LX5kZtLACAxJPkcDhz80OwZ88ekwmhwYizzjpLsrOzpWHDhjJp0iS54oorzDKrV6+WTp06ybx58+S0006TL7/8Ui666CITvGjcuKQn9xtvvCEPPvigWV96enq5z6u/XugOgj5f7dq1Qx0+IuR3f58jP2/Nlndu7CnndSztuz7snQUye21JZfD5I/tJk6zAdgwBAIHh+zEwn3zyiVx22WWSkpLi9iOKBtOTk5NN1oT9PqW36cm+rTVQUVW39Zb9R+XM5771ef9Ht/eRXq3rRXVMAIDE2BepUE0KfWJVr17Jl9TixYtNdoV9bmfHjh2lZcuWJkih9Lxr166uAIUaOHCg+cesXLnS6/OQYllJC2eWqUnh/TIAANHUr18/Wb58uSxdutR16tmzpymiqZc9AxQqIyPD7GzZT1VZi3rV/d5/5Rvz5M3Z66M2HgBA4kgN9YHFxcWmn3jfvn2lS5cu5radO3eaTIg6deq4LasBCb3PWsYeoLDut+7zlWI5evToUIeKGAUpyrYgtReliPaoAAAoobW1rH0XS40aNUw9Lc/bE1mDmumy93C+z/vHfrlahp3eWjLTygZ1AAAIVciZFFqbYsWKFabwVKSNHDnSZG1YJ51LivhV7LO7h/0yUQoAAOLZ53efWe4ya3YeispYAACJI6QgxZ133ilTp06Vb7/9Vpo3b+66vUmTJqYg5sGD7j20tbuH3mct49ntw7puLZPoKZZVZrqHn0wKghQAgHgya9Yseemll2I9jLjSuHamrB5zgd9lLhn3Q9TGAwBIDEEFKbTGpgYopkyZIjNnzpQ2bdq43d+jRw9JS0uTGTNmuG7TFqXacrRPnz7mup7rPNDdu3e7ltFOIRp46Ny5c8X/RYg5Z4yi7HQPH5cBAEB80qkcf7mwo99lhk9aIlsPHDX7iQAARDVIoVM83nvvPdO9Q+dzag0JPR07dszcr1U8b7nlFhkxYoTJstBCmjfddJMJTGhnD6UtSzUYcf3118uyZcvk66+/llGjRpl1a8YEqnLhTDIpAACobG476wS/93/+8w4549lvZczUX6I2JgBA1RVUkOL11183NSHOOeccadq0qev0wQcfuJZ58cUXTYvRIUOGmLakOoXj448/dt2vFbN1qoiea/DiuuuukxtuuEGeeOKJ8P7LEIeFM+1XojwoAAAQslVPDCx3mXd+2BiVsQAAqragunsEksaXmZkp48aNMydfWrVqJV988UUwT40qUTjTnkkR9WEBAIAQVU8PuSEcAADR6e4BlF840+MOewdSpnsAAFCpfHlP+d0+VmzLlsKi4qiMBwBQNRGkQOQyKTzSJcikAACg8urUtLb85/aSQui+XPTqHGn78Jcy4YeNFNIEAISEIAUiV5PCI1vCvSQFUQoAACqbnq3rBbTc6M9WydSfd0R8PACAqocgBcq1Zf9R+cf3G+RIXmGQ0z08MylKLzPbAwCAymlw16YBLffz1oMRHwsAoOohSIFyvTpznTz5+S8y9eft5S6bV1gkzhhFOYUziVIAAFAZPX1514CWe/v7jXLrPxdFfDwAgKqFIAXKdfBogdu5L2/OXi9dH/tGDjszLjxrUrgXzozAQAEAQMRlVUuT14eeEtCy01btivh4AABVC0EKlKvAWaXbOvdl7JerJd+2TNnpHmRSAABQFQzq2lTWPTUo1sMAAFRBBClQLivwkF/oP0jRoGa623X/hTMBAEBllpaSLBNu7FXucj9tPhCV8QAAqgaCFCiXFZzIKyeTotAqRuGU7PHqsmdPkEgBAEDld0a7BuUuc9lrc6MyFgBA1UCQAuXKLyoJPhQUOvwWzPSsWVE9PdXtuj0wkUSUAgCAKpFNUS0tpdzlWj/0uew9nBeVMQEAKjeCFAg4kyK/qMjnMnsP55vz9JRkef/3vU36Z80MzyBFSWDCs54mAACovGb86eyAluv55HS58OXvZcHG/REfEwCg8iJIgXIVBFCTYndOrjlvWCtD+rZtIOd2bFRmGSs4QdFMAACqjmZ1qgW87KodOXLVm/MiOh4AQOVGkALlsoITBc5pH97sPlSSwtmgVobPZazYBDEKAACqluWPD5A3r+8R62EAAKoAghQIfLqHv0wKZ5CikZ8ghZVBQT0KAACqllqZaTLwxCayeswFsR4KAKCSI0iBwKd7+Onuscc53cNfkMIKTRCiAACgasoMoIimeuu79REfCwCgciJIgTBnUmT6XKa0cCZhCgAAqqo3rjul3GWe/mJ1VMYCAKh8CFKgXHlFQQQpalOTAgCARHZBl6aycvTAWA8DAFBJEaSAXw6HwzXdwzr3Zk8QNSnIpAAAoGqr4dGG3JtZa3ZHZSwAgMqFIAX8Kix2iMPZ1MNfTYrdh3LLn+7hcQ4AAKquF67q7vf+GycslFv/uUi2HTwWtTEBAOIfQQr4Zc+e8DXdo6jYIXsP55c73SM52eruEfZhAgCAOHPZyceVu8y0Vbuk7zMzozIeAEDlQJACftkDE74yKfYfyTeBCg0+1K+R7nNdVnDCClYAAICqK5iW47PX7pGDR/Plg4WbJSe3IKLjAgDEN4IUCDxI4SOTwprqUb9GhqSm+H5JJTknehCiAAAgMax9cpCkp5a/uznsnQVyx3tL5MH/Lpf7Ji+NytgAAPGJIAX8smdPeAtS6DzSd+b8Vm7RTGUlUFA4EwCAxKABCg1UBGLehn3mfMZqCmoCQCIjSAG/7IEJz+4e2vnj8td+kP8u2Wqun9qmnt910YIUAIDENH9kv6CWX7k9O2JjAQDEN4IUCDmT4kh+kezKKWk9On5YT3n0os5+12VlUAQzRxUAAFR+TbJ8d//yZvArc+SvX6+WI3mFERsTACA+EaSAXwWFDlsmRellpQWuVEZqsvTr1Ljcgpi0IAUAIHFN+ePpQS0/7tv18rdv1kZsPACA+ESQAn7lFxXZLhebKR6Wg0dLqm/XqZ4W0LqsDApqUgAAkHhObllXVoweGNRj3vlho+w7XJK1CQBIDAQp4Fe+LZPCc/pH9jFnkKKa77ajdlZwgg6kAAAkphrpKUE/5o73l0RkLACA+ESQAn7ZgxKeUz6sTIqsgDMprHOiFAAAJKJQ9gEWbNwfkbEAAOITQQr45Vks03794LGSmhR1qgUWpLAyKIhRAACQuF4bekqshwAAiGMEKeCXZ9tRtyBFiDUpCFIAAJC4LuzaVP7+fyfHehgAgDhFkAJBZVLYgxZWd4861QOrSWEFJyicCQBAYruoW7Oglm/90Oeyce+RiI0HABA/CFIgqCBFnpdMiqwAp3skOZuPEqIAAADBtiQ99/lZMm3VroiNBwAQHwhSIKjCme41KYKb7mHVpCCTAgAAnNSiTtCPufWfi+SL5TvcWqIDAKoWghQIebpH9tHgWpC6YhPEKAAASHhaq+qXJy6Q8zo2Cupxf3x/iTz1+S8RGxcAILYIUiC4TIoiL909As6kKIlOkEkBAABUtfQUefuGnvKvW04N6nH/mLMxYmMCAMQWQQr4VeCvBWmwNSlcQYqwDhEAAFRiKclJcma7hjLm0i6xHgoAIA4QpEBImRQ6FzTYmhSlsz2IUgAAAHfXn9ZKRg3uFOthAABijCAFQiqcmVtQ7LocaAtSK4OC2R4AAMCbG/q0lo5NasV6GACAGCJIgZAKZ1r1KFKTk6RGekpQ0z2scwAAALv01GT56t6z5Mt7zix32d2HcqMyJgBAdBGkQFBBCuu6VY9CsygCDTqUtiAN9ygBAEBVotkU+kOIP6ePnSlTf94etTEBAKKDIAX8srcc9R6kCKweheHKpAjnCAEACN7rr78u3bp1k9q1a5tTnz595Msvv4z1sOCkP4D8+vSFfpcpLHbInZN+kv8u3ipXvD5XvlqxM2rjAwBEDkEKhDbd46iz/WiAnT3cMymIUgAAYqt58+byzDPPyOLFi2XRokVy3nnnySWXXCIrV66M9dAQpD99tEwWbTogt7+3ONZDAQCEAUEKBFU4M8/KpAiys4c9OEFNCgBArF188cVy4YUXSrt27aR9+/by1FNPSc2aNWX+/PmxHhpsnh3SNdZDAABEGUEK+JVf6HC/7sqkKAlSZFULrLOHewtSAADiR1FRkUyePFmOHDlipn0gflzdq6Vc3bNFrIcBAIii1Gg+GSofKyiRmZZs2o4WOIMWVnePUDIpKJwJAIgHy5cvN0GJ3Nxck0UxZcoU6dy5s9dl8/LyzMmSk5MTxZEmtuHntpUPFm0JaNlZa3bL375ZKxd1ayoNambIkB7NIz4+AEB4EaSAXwXO6R01M1IltyBf8ouKzPVsq3BmEDUprBQKpnsAAOJBhw4dZOnSpZKdnS3/+c9/ZNiwYTJ79myvgYqxY8fK6NGjYzLORNeyfnU5v3NjmbZqV7nL3jhhoTlfvi3bnJ/Sqq60aVAj4mMEAIQP0z0QUCZFjYxUt0Kaa3cdMucNa2UEvC4yKQAA8SQ9PV3atm0rPXr0MEGI7t27y8svv+x12ZEjR5pghnXasiWwX/YRHud3ahzS43bn5IZ9LACAyCJIAb/ybZkUqqDIITuyj8mSzQfN9XM6NAqhJgVRCgBA/CkuLnab0mGXkZHhaldqnRA9l51yXEiPu+ZtCqECQGVDkAJ+FXhkUmh3jy+Xl/Qh79mqrjTJygx4XcnOVxuzPQAAsaaZEd9995389ttvpjaFXp81a5YMHTo01kODF2kpyfL9n88N+nEOh57ci4ADAOIbQQoEmUlRLJ8v32EuD+7WNKh1WRkUBCkAALG2e/duueGGG0xdin79+snChQvl66+/lvPPPz/WQ4MPwUwxtbtn8tKwjwUAEDkUzoRfmjlhz6TYvP+oLN50wFwe1CW4IIV2CFEZqSlhHycAAMEYP358rIeAIGWmhbb/8Omy7fLw4E7SuHbg2Z8AgNghkwIBTfeomVGyY7Dwt/3mvHvzrKCmeqgz2zWUm/u2kbvOaxuBkQIAgKruihBbivZ+eoZ8sHBz2McDAAg/ghQIqLuHNd3DmtbZOoR2XpqN8ejFnaVn63rhHSQAAEgIp7SsG/Jjn/5itazYlu36AQYAEJ8IUsCvAo/pHhZSJgEAQLSd0qpOyI/NPlYgF706R+6lRgUAxDWCFAgqk8LSKMTiVQAAAKHq2KTirV+tAuC3/XORtH7oc8nJLQjDyAAA4UKQAj4VFzukoMjhPUhBJgUAAIiBdo1qVngdRcUO+WbVLnO52+PfSI8x0+Tdub+FYXQAgIoiSAGfCopL52yWme5BJgUAAIiB937fu8LrKLaKbDntO5Ivj326ssLrBQBUHEEK+JTvrEfhLZOCmhQAACAWdB9k49gL5YPbTgt5He0e/tLr7YucXcwAALFDkAIBBSk8Myka1SaTAgAAxEZSUpL0Pr5+2Nc70Tnlw74PBACI8yDFd999JxdffLE0a9bMfEF88sknbvc7HA559NFHpWnTplKtWjXp37+/rFu3zm2Z/fv3y9ChQ6V27dpSp04dueWWW+Tw4cMV/9cgrKx6FKnJSZKRWvpSqZWZKtXT3YMWAAAA0fbGdaeEdX1rdx2Sq96YJ+1HfSkfLdpialfsyD4W1ucAAIQ5SHHkyBHp3r27jBs3zuv9zz33nLzyyivyxhtvyI8//ig1atSQgQMHSm5urmsZDVCsXLlSpk2bJlOnTjWBj9tuuy3YoSDCrF8R0lOTzcnCVA8AABAPmmZVC+v61u46LAucUz4e+M/PpgNIn7EzZcYvJUU2AQCRF/TP4YMGDTInbzSL4qWXXpJRo0bJJZdcYm775z//KY0bNzYZF9dcc4388ssv8tVXX8nChQulZ8+eZplXX31VLrzwQnn++edNhgYi597JP8nWA8dk8m2nSWpKckDtR9NSPIMUTPUAAACx17pBjYiuf8bq3eZ8/JyN0q9T44g+FwAgAjUpNm7cKDt37jRTPCxZWVnSu3dvmTdvnrmu5zrFwwpQKF0+OTnZZF54k5eXJzk5OW4nhOazn3fIok0HZMuBY8FlUtgCGo1rkUkBAABiL6tamswbeZ7c1799RJ9n3e7DcjS/UGau3iW5BUURfS4ASHRhDVJogEJp5oSdXrfu0/NGjRq53Z+amir16tVzLeNp7NixJthhnVq0aBHOYScMnVepJ5V9rKDc5a1MinSPTIpGTPcAAABxNOXjnv7t5NKTIpeNu+dQntw16Se5eeIiGf0ZrUoBQBK9u8fIkSMlOzvbddqyZUush1QpFTiDDiongCCFtbwGKHTKh4XpHgAAIN6MubSL6/KjF3WO2NSPfy9gPxQAKk2QokmTJuZ81y734kJ63bpPz3fvLvmQtxQWFpqOH9YynjIyMkwnEPsJwbMyIzwzKT5eslWe+nyVrN6Z4326R5maFGRSAACA+FIrM00GnthYzm7fUH4XwawK9e8FmyO6fgBIZGENUrRp08YEGmbMmOG6TetHaK2JPn36mOt6fvDgQVm8eLFrmZkzZ0pxcbGpXYHIsff8zsktcN324H9/lre/3ygXvPS9vPDNmvJrUpBJAQAA4tCb1/eUd28+VRrUjOy+ysiPl8v36/bII5+skOyj5WenAgAiGKQ4fPiwLF261JysYpl6efPmzZKUlCT33nuvPPnkk/Lpp5/K8uXL5YYbbjAdOy699FKzfKdOneSCCy6QW2+9VRYsWCA//PCD3HnnnabzB509oheksDIpth08JgVFJXUq1JSl28y51q6Y5my3lWGmeyRJclLJMo0onAkAAOKcdjKLpOvHL5B/zd8kf/pomSz8bb/pcgcAiEEL0kWLFsm5557ruj5ixAhzPmzYMJk4caL8+c9/liNHjshtt91mMibOOOMM03I0M7P0wPb99983gYl+/fqZrh5DhgyRV155JQz/HARek6LQnG/ad8SVHbErJ0+27D8mh/MK5bH/rZT/Ltlq7ru6VwsTgBp+blvZdyRfmtcNb09yAACAcDvt+PpReZ7pv+wypzeu6yEXdCk7dXnL/qOyZuch6depkdmfAgCEOUhxzjnn+I0U64fvE088YU6+aCePSZMmBfvUiEAmxeb9R815t+Z1ZPnWbNmZkyvz1u9zBShevuYkueSk48zlPw3oEJNxAwAAhOLyk4+Tj38qyRKNtNvfWyy/PTO4zO1nPvetOf/HDT2lf2f3DngAgEra3QPhL5xp1aTYtK8kSNGqXnXp0KSWufze/E3mvEPjWq4ABQAAQGVzXZ9WEi8Wbtof6yEAQKVAkCJRC2c6Myms6R6t6leXjk1LghSz1+4x5z1a143JOAEAAMLhlJZ15bGLw9+O1JetB47K7pzcqD0fAFRFQU/3QOVlL5BpTfdwZVLUr2Fad9n1IkgBAAAquZv6tjG1uF6cvjbiz3XGsyVTO+7u106m/rxdRpzf3u3+o/mFZh+saRb1vQDAFzIpEjiTorjY4apJYc+ksPRsVS/qYwQAAAi3e/q3kw1PXxi153tlxjrZsOeI3DnpJ7fbT3t6hvQZO9MU0wQAeEeQIkG7e2gUf/ehPMkrLJaU5CRpVqeaHN+gpmk1anX7oIsHAACoKpKtXuoxkiRJkpNb0l1t3Le/xnQsABDPCFIkEA1IWPRL0qpHcVydapKWkizpqclyQsOa5raerevRJgsAAFQprw09RTo2qSXX9GohL17d3dw2/NwTovLcHyzc7Lo8eeEWyS0oisrzAqi4f3y/QS58+Xs5cCQ/1kNJCNSkSNBMiqJih/yyI8c11cNyapt6snrnITm7fcOYjBEAACBSLuza1Jws53duIjUzUqVaWoo8/01ka1YcOFpSD8xyNL9IMtNSIvqcAMLjyc9/cWVBjbooesV4ExWZFAlak0L9vC3bnLesVxqkeGBgB/nXLafKlT2aR318AAAA0aQBCnVVrxZRf+5TxkyTbI/ABYDKk5mOyCFIkUDybZkUatmWg+a8df0artu0w8eZ7Roy1QMAACSMRrUyZfywnlF/3u5PfGOmgTz71WpxOEq7sAFAImO6R4JO91Dr95TUpGjfxL2rBwAAQKLp1rxOTJ73wf8uN+eb9x2Vv//fyfxQBATpcF6hKysKVQOZFAk83cPSiSAFAABIcA1rZcj0EWfJvJHnybkdol+b6/PlO+TTZdtd18msAMr35NRV0uWxr2X22j1hW+f0Vbvk9LEzZMHG/eb6D7/uDdu6ERiCFAk83UPVq5FuvpQBAAASXdtGtaRpVjV558Ze0r15lvRtWz+qz//9ur2SV1gkb8xeL21GfiHTVu1yFTz/39JtsnFvSRaspx3Zx2Q/XQeQgP4xZ6M5H/tFSWHLcPj9PxfJ9uxcuW78j+b60H+UnNtpPZnf/X2OjHc+f6AO5VKHJhAEKRI8k0LbcJFWCAAAUEr3jf535xny/u9PM0XFo+U/i7dKh1FfyTNfrjbXb/3nIhOYOOEvX8g9k5fKuc/PKvOY7GMF0mfsTFOIE0gEew/neZ3Kbncsv8i0Df3NR2CvIlno6o3v1svPW7NlzNRVQWV9dH38G/l2zW6JhU9+2ibD319SKdofE6RIIN7eyB2b1I7JWAAAACqDWLdl9wxMvPDNGreMioochFVVOlWG6TKV27aDx+S0p2eYQJ2VUaQm/LBRej45XV6ZsU7esWUxWD+66t/94NF8GfTyd6Zt6Ll/m2WCGpqhFIobJyxwu64H+Jq5tDunJFCiFm/aL7sP5crR/MKAsj5umrDQ7fbComKTKbUzO9d1m8Ph8BskCeU1fu8HS820solzf5N4R4WRGBR2mfvrXjmrfcOo98b2mknRlHoUAAAAlcUrM381py/uPlM6N6stJMRKmQO+i16dI83rVpd/xKBjCypmyeYDMnXZDvllR47szMmVnatyTZCiZb3q8vsz28joz0oyF16YttbtccnO98Ef/rVYvrEFNfQ4XoMazetWkzkPnhf0eGatca918dHirebkOU3rpenrpEZ6iqx84gIz9SozLVmqpwd2qP3ODxvl6S9Wm+KfK0YPNLfd+s/FMnvtbpk/sp/Ur5nhdjynGRFvfrdeamamyZQ7Tje3L9t60HweZKSmuGWTPPn5KrmgSxPTvdFy4Gj8Tw0jkyLKXp/1q9z2r8Xy4aItUX/ugqKSaFt6aumfvROZFAAAAJXOha98b86fnBq+ufhVwbKt2bJ65yGZ/kvpgarl7zPXyYgPl5JlEac0S+Hy1+aag/Z5G/a53bd5/1F59H8rfT422Rmtswco7LYeOCbz1peuU18D//f2fOn7zExzubjYIcu3ZvudQuLLlJ+2mfMj+UVy0avfm6lXnR/9Wj7yc7ynwTTNwBjw4mwToLB+zG790Odmmoq+fvXYzV5MV7313Xr5839/Nl0al205KD9vy5Y+z8yQy16bK8Pecc/60No27/+4Wa4fv8AELEr/8SXFQJ/6fFXcvhcIUkTZDmcaz/aDpek80ZLnzKRo6IzGacSxXeOaUR8HAABAZdGqfnXX5U/v7CvxROfDL/itpANBPNODLz0oi7Xnv1krHy/ZJos2HZB4te9wniyqBH/TSHhv/qaQHxtIRtG1b893HZTrdJK56/eZ8/8t3S5///ZXufjvc+RPHy4L+rk37TvqurxiW47r8gP/+dmca7BCO5DYXfHGPBny+jxZu+twmfU9+Xlp4FEzR/S9o9NcNCjxw6/uwZtLx/0gu5xTT+Zv2C/frt7tqjnx8ox1ruXOf3G263Kxw2GKgb79/UZToHfqz+6BkHjAdI8os6JY5c1ZigQrMqjdPPQN2bp+jahPOQEAAKhMamWmyYK/9DOZqHWqp8uPf+knvZ+eIfHAs7OAHoDFW0H0A0fy5eQx0+T4BjVk5v3nmNvW7DwkX63YadL3a2SE93AkkH9+XkHsAya+nP7MTPPD4rs3nxrzeijRVpFpCPpn35VT/o/A/5y3SYad3tqtrou2L7WyITwzFypq3a5DrmCF3dItBwNeR9uHvwx42ZsmltS7mHzbaWUySSwanLC7c9JPclG3ZhJPyKSIsmPOyNaRvOhXVbVqUpzToaH069hI7urXNupjAAAAqGwa1c40AQrVuHamrHnyAolHN7yzwPxi+6/5m0wquaaOa3HB79buMens5dl+8Jhs2hd6IU79xXfFtmzTMtV+AKg27D1i2jbqD3YDX/pOXpy+Vk587GvX/cE8h7+U/EBCNFb9AstPmw/I+j1lf9GOBSvzebZHLYREkBTQX8/3NJ9AgofvOotG6hQIixWgiITzX/xOYuGat+ZLZUaQIsqOxkEmhRZfGX9jL7ns5OZRHwMAAEBlp8Xp/nD28RJvtICfTq145JMVJpVcPTF1lQlelHcgplkY+iv+2X+dJTm5BSE9/8NTVpiilc9/s8Zc12CFdhSwdH/iG+n6uHvau86j1+BFIDTQ0uup6dLu4S/d59j7aenqjT3bRDs16Hz+fn8rTYePB3GWEBNWc9fvlcte+0FWbS+dGvHr7sMRDRZYNFimwTu4C7X7SaQQpIgya46QFlaJVSZFekoV/tQDAACIgpGDOkll4llIUvdJh/5jvpzwly/MQeO63aWZBPZWiPZpG3qyfP7zDnOwd5OtReMHzkKBr89a76qv4KnQS0aHBi8sew7l+Szml32sQA44Axp3vL+43ADE/R95ry9wy7sLXUUNN+6JzxauVXlv/f/e/lF+2nxQbnZOTVD9X5htpqMjNqYsiXyAKBjUpIhVJkVe9DMp8p2ZFPbuHgAAAKj6vlq50xz8ax2Lb9fslq7H1XEV4dODRrsNe47Ihwu3mF/zHxjY0UwX+f0/F5n7pt51hnQ5LkuGT1pirn/rZ1rC299vCHh8u3Ny5Y3ZG0xnh1vOaCOPXNS5zDJrdx1yaw158Gi+axqOZn90H/2NW6tFf/vjWifgyp4ttNFBGZrZUbtaqmg8RaeGeKvzMXnBZlmxPVue+F0XSfacP4KA7HUGsX6z1YdAbOyzBSDjAUGKKLNS02KZSZGWQpACAACgor7/87mycnuOdG2eZVoZntCwhtzbv73c9e+fJN5ocsLXK3e6Ogd4dgmwe/R/K2T3oZIDyC9X7HQruvfXr9e4FR20UsUf/9S9PeTiTQfKFOjz51RbPQENpHgGKV6evs7UsbA76YlpctnJx8npJ9R3FSfUgEowPJM2dBv94V+LXT/sdW+eJR/dfrrbMpqhYU0lOf2EBmYdfdvWNwET3dfXv7/W9vjo9j6uIEqwqvJ0D8+smtvf854Vg+gJZPpUNBGkiFHhzFjUpHBlUhCkAAAAqLAW9aqbk9JimrqPpb+666HX3XEYqLj9vZLsh/JYAQplD1Aob4Uuh779Y5m2nv8IIovCm/1H8qVejXTXL+2eAQqL1jHwV8tAp7Vo0KVfp8Ze79d2jPYMCitAYf3At/C3A35rXWj9D/0VulPT2vLlPWfKgJdmy5b9x1yZJJqJ4o+vjiy+urTo8o99ulKOq1NN/nD2CVLZ6dSg3ypQrBXhsW53aZZSPOBoNVaZFDHo7mEVzmS6BwAAQPiLaVoHlr/r3kweGNjB/MKfCDwDFFYGRkWcNnaGz0BJMDo+8pV8uGirW/DB7oOFJbUpPGtj2GlWhE7v8NZVxEqT/2VHSRFIK0Chxn1bUpvD4llr44OFm00hUO2I4slXIsXybdmmjebYL1dLPNP6EppNU55nv1otuXHcEjZRHDgSWrHcSOFoNYq0InFMMylchTP5swMAAETS8HPbyqRbT4v1MCot3W+1Dt6vG+9eMyNc3v5ug3y+fEe5y2nHk4c+Xi4TfyhpXxmMKT9tla0HjpqWsBqQ+N/Sba7jggf/u1z2Hs6XER+WdkApL0qh3VuiScd9zVvzZPnWbFn0236/7V/tdPrTkNfnuoI3iG8Lftsv8YSj1Rj0PbYK9gTSrzqcCopKno9MCgBAohs7dqz06tVLatWqJY0aNZJLL71U1qwpaZsIRMo3950l53f2Pu0AZWk700vG/RCx9T/1RUl9jkD9uNH/gZy31q33fbBMznj2W7lxwkITkLhncklAYsgbc/2u683ZG2Tqz9tdnQEtPhqfRIy2sp2/Yb9c/Pc5csUb8+SJz1YF9XgNbhTaAhvvzd8UgVGiquFoNYo8sydyo9yPlsKZAACUmD17tgwfPlzmz58v06ZNk4KCAhkwYIAcOcLcaITX/JH9zPnDF3aS9o1ryds39JTfnhksCx/uL7PuPyfWw4t7y7YclHhhr1/hTbfHvU8XUYdyC91qX2gLTkuSj7SJOyf9JKOdQYHv1+0xBTlj3YXhX0EGGVbtyJH2o76UF6etNZkroz5ZEbGxoeqgcGYUWVM9LFqXonp69P4EtCAFAKDEV1995XZ94sSJJqNi8eLFctZZZ8VsXKh6mmRlmqCEp4a1MswJlcfM1bvLdDYJxSXj5rhdP1rgewrHvxdsllvOaC3Xj19grv+4YZ9b21atgxIvr6MJP2yU+Rv2yd//7xTXbRPnlkyReXnGuhiODJUNR6tR5JmuFe26FGRSAADgXXZ2ydz3evXqxXooSDCrx1wQ6yEgCOc+P6vC6/ht31G361axTV9tIPu/8J3XzivatlXrXLzwzZqo16rwRrM+vl65S/63dHush4JKjkyKKNI6FHbR7vBhFbrJIJMCAACX4uJiuffee6Vv377SpUsXr8vk5eWZkyUnh2JwCI/MtJRYDwFx4E8fLpP/LiltbRqMV2b+KgeOFsiYS7u4dRIZPmmJybQYZ8tsCIe/z1xnutlcevJxptXuzWe0kf6dGrnuPxIHARNUbgQposgzOkomBQAAsae1KVasWCFz5rinYHsW2hw9enRUxwUgcYQaoLDXirixb2t57qvV0qxONbnljDbyxfKSNrCNa62SEQPaS80M74d+93+0TPYezpP7+reXRrUzpGlWNXP7Qh8dH57/Zq1b4dF5G/ZJ9+ZZrvsf+3Rlhf4tAEGKKDrqWZPCR0pXJGgnkUJnNxFqUgAAUOLOO++UqVOnynfffSfNmzf3udzIkSNlxIgRbpkULVq0iNIoUdUtf3yAnPrUjDL1y4Bg9PvbbLdaFpZ3ftgoeYVFMvp3J8rybdkmu7tv2wau+/+zuCRAMmvNHnO+5skL5PFPV7mtozzLtpZMmQPCgSBFFOV6ZlJEMRXKKpqpCFIAABKdpkLfddddMmXKFJk1a5a0adPG7/IZGRnmBERCrcw0+WXMBaYQotYZACoqt6B0319pN5EBL34nG5yFP6ePOFvaNqppPgs9/eP7jUEFKFD5XdHDd5A+FjhajWVNiihmUtiDFGkp3tscAQCQSFM83nvvPZk0aZLUqlVLdu7caU7HjpUUsANioVHtTNk49kIZP6yn1/vP79w46mNC1aCtQK0Ahfp19yGTae2tq+o3K0umicSj14aGt74GSgzq0kTiCZkUUeSZwhfNmhQFznoUKp2aFACABPf666+b83POOcft9gkTJsiNN94Yo1EBYgod9uvUWKb88XS57LW5rts/Gd5XTmpRxxRCb/fwlzEdIyq/299bUmmmbrx09Uly7wdL5ZVrT5YLuzaN+vP/944+8p/F2+SkFlny6+7DsnrnIfl+3V6pzC4/+Tj5+KdtcuuZbeRPAzrEXQFfghQxLJwZze4eViaFZlHolx8AAInMW4ozEE9ObllX3rult7z9/QZ56rIu0rxudVcB9BWjB0qXx76O9RCBqNAuInqyfHR7H7nyjXlRe/4ereqZk11RsUOWbT0ol782V5KTRJyl/3zq17GR/Lhxv1ur2Fn3nyPnPD9L6lZPk1vPOl7mrd8nZ7Vr6CpI6s/jF3eWFdtz5LyOjWTgiU2k399myXF1q8kFXZrKBSc2kWe+XO1WjFWzsKat2mUua4HUe/q3k79d1T1ujwuTHJXwW1qLVWVlZZme5rVr15bK4pUZ6+SFaSXVcNVd57U1kato2LzvqJz112+lRnqKrHyCftwAUBVV1u/HyohtjVjTXfj35m+S93/cbAoiXv3W/FgPCQi7CTf2knM7lrY3tazZeUiuenOeZB8riNhzax2/6fedLS3rlwQIvfllR440zcqUOtXT3W5v/dDnrst/vaKbDDmlufy8LVt+/+4iObFZbXnkok7StlEtv89/OK/QvMfbNqwpWdXTpFfrerJl/1GT9dCwVvA1krR46qrtOdK9eR1J1shKHH8/kkkRy5oUUc2kKHmuNIpmAgAAVHr6C+j1fVqbk/rtmcHy89aDsmDjfnny8/J/iY01/QV45urdsR4G4pAe0D9/ZTe/v/J3aFJLlj02oMz0pzeu6yGnHV9P7p68VL5bW9KtJFDf3HeW5BYUSZdmWQEfxHdq6v0g/O0beso7czbKX6/s5sqC0ulaCx/uF3D2Qs2MVLn97BPcbmtRz3fApDwZqSkmQ6syIEgRRfqiV1ZK0LGCKHb3KHS2H6UeBQAAQJXUrXkdc/r9mcfHde0KneOv44zX8SG27jjnhIAP5HX6kwboNKNCM8ZTncc6/7z5VHlp+lrTVlXru+jcAa0P+Omy7TLy4+Wux2emJZtOKD1a1ZX2jf1nNgRDp1d4K3Qbr9Mr4g1BiiiyCmXWq5Euew/nx6gmBUEKAACAqk73+a47raW8Nz8+Wkm2ql9dnry0i5zZrqHrtueu6CZ//s/PMR0X4k/DmsFPZciqllbmtnv7tzcnpbGBGhmpcu2pLV1BilGDO5mAHuIPQYooOubsV9ygZoYJUkS1u4czSJHBdA8AAICE8OSlXeMiSPHTI+dL3Rruc/bVVT1bEKRAGTUzI3uIOn9kP1nw2365MM7abqIUR6xRdMwZlKhfs+RDOqqZFM4WpFoABgAAAInh3ZtPjenzf33vWV4DFJY/nV/yS3egdNr0hqcvlI1jS076aziqjnaNakpKBIs6qiZZmfK77s1cU0MQf/jLRJHOg1L1a5SkMEUzk8IKUjDdAwAAIHGc3b50ekV5/je8b1Drvqpnc7/3a60ALXDoz+3nnCAvXt1dxlxyYkDPuWHsYFPUUOf26ylS6fqD+JU96j6/+wz59M4zYj0MxAGOWGPQ3cOVSeHR7SMaNSnIpAAAAEgsi0f1l+7Ns/wu8987TpfuLeqYOhaB8uw8MLhbU5k+4ixz+fdntAloHfoD2mUnNzddSr69/xy/y1rr9mfSrb3l+tNayRU9/AdQbujTSib9vrfr+lf3num6/NRlXeT163oENH6Exy1ntJETm2VJtfSUWA8FcYCaFFF0zBmU0JoU6mheLDIpqCgLAACQSOrXzJD/3XmG7MrJld5PzzC3/eOGntKrdT0Z8/kqc1CvAQqrjsVNfdvI4k0HpM/x9eXM5751rWfOg+fKjF92S/X0FBMEyHPuX1rr69epkcluWPfUoJCyd9s0qCFNszJlR3aumSYy8KXv3Fo9tm3kPSvjnn7t5OUZ6+TG01vL6Sc0MCf1/JXd5XBeofxr3iZpVCvDtG88tU29Mtkelgk39pJtB4/J0N6tzPVuzbPk563ZQf87EJhXrz1ZftmRY1rR3tO/XayHgzhCkCIG0z0axCCTwiqcmZ5KdBIAACAR1c4s7YBwRrsGkpmWYg7kPZ3QsKY5qb9d2V0e/2ylTLyplzSvW12Gnd7atZw+/oPbTpNih0ifE+q7bq/I9OJ5I/u5Ln8yvK9c9eY882Pbf27v4/Mx9/ZvJxd3byrHNygZs13NjFTT0jIQ53Zs5HZ98m2nSedHvw5q/Cjf+qcvlCP5heb1eHH3ZvLnCzrGekiIMwQpYplJEYOaFOlkUgAAACQkTaXXef9JkmQCDIEY0qO5OfnS+/jS4ES4ndSijqx9clC5y2n2hq8si4qonp5qMi1aP/R50I8dcX570b3uv01bG/ZxVWbfPXCuKYxpD5gBnghSxCBIoSl3qqDIYYIH0agTUZpJQU0KAACARKXz/hGcs9o3lO/W7vG7TEZqsvxf75Zm6smq7TlymjN4c0KjmtL1uCypXS1Nuo/+RhJdy/rVYz0EVAIEKWLS3aO0DZNmU6Sn+m7LFC7WnEG6ewAAAACBe/7KbnLqUyW1PCza+rR/p8bSqHaGybiwO71tSU0MdWHXpq7L9/VvLy9OXysDT2wsrRvUkDdnb/D5nK8NPUX++P4SqSo0s2TgiXRMQWAIUkSJZkwU6oQ953xAzWjQ27TjR50oBBQ1a0OlE6QAAAAAAtaoVqYpGqr1LepUD/3HRS0OaS8QecfZJ5h6HqeMmea67aa+reXBCzqa6ThagFSLSv7hX4ulMrMXJwUCQZAiylkU1nzABjXSZXt2rizZfECa1akWvZoUTPcAAAAAgqJFQ8PNCnjMG3me9Bk701x+7OITXfdrBrRmH2wce6HJirbqiJz53EzZsv9Y2McDxAuOWKNcj0ILxWgb0Ct7tjDX35i9XhyOkiyHaNSkYLoHAAAAED+aZlUznUSm3nWGz8Kg9kKnWnxyyh9PN8ENXx4Y2CEiY23tUVPiqp7uRVVn3X+OvHl9j4g8NxIHmRQRdiSvUGpkpLoyKaqnpZgPGm3f9OZ362XFthz54dd9pg1UJOU7gxRa1AcAAABA/LAKbQZCjyVOblnX5/1aL+P3Zx4vV/ZsXqaWRiiWPnq+OZZoUCNDkpOTTIZ2UbFDNu0/Ih2b1JbOTWvLE1NXybQRZ5taG3qyjwUIFkesEaRZEic+9rV8tGiLq91oZnpJFLRejXS5pldLc/nJz1fJ7pxcWbzpgMxeu8eV9RCJ6R5kUgAAAABVy1OXdTHBguPqVDNdRqxaGhWtB/H2DT3NtBRdlwYorOnjOn1dAxTqxr5tZMPYwXJCw5qux0277ywZ/bsTzQ+zQLDIpIiQLfuPygvOvshPfv6LPHdFN3O5ujNIof5w9vHy6bLtsnrnITnjuW9dgYSmWZlu7aGaZGXItae2rFDLKCuTgpoUAAAAQNWgxxNz1u2Vy09uLkN7twrruleMHmiKhYaiXeNa5gSEgiCFiGQfK5DlW7PDus53ftjoCjro+u+cVNJCSKOb9vlnOp/spokLZcOeI5KZlmxaGO3IzjUnu/fmb5ZTW9czkdEGNTOCGsuR/EJZ/NsBc5lMCgAAAKBqGDmok8gg/8v8/PgAyc0vkka1M2Xl9mwZ/Mqcctf72Z1nhBygACqKV56I/Lr7sFw3/sewr1eLZD56UWd57NOVpgWoBig07cmuVf0a8snwvvLDur3S54T6JnXq29V75ODRfHO/ltScu36ffLl8hyz4bb85hSopSeTEZiVpWQAAAACqvtqZaeakNDN72aMDZOry7TK4a1Nzmx5r9OvUSH74da/cPHGRua1r89AzuIGKIkihLUHTdE5V+NORtIOHzsPSTIa1Ow/JXwZ3MvO5POmHxiDnh4S6oEsTt/t1qsfOCzvJ+z9uMr2StVBNsHq2rivD+rQm7QoAAABIYFnV09ymhlzoPA45p30jufSkZtLlOAIUiK0kRzT6X4ZZTk6OZGVlSXZ2ttSuTWYAAACK78foYVsDABCZ78eYFigYN26ctG7dWjIzM6V3796yYMGCWA4HAAAAAADEUMyCFB988IGMGDFCHnvsMVmyZIl0795dBg4cKLt3747VkAAAAAAAQCIGKV544QW59dZb5aabbpLOnTvLG2+8IdWrV5d33nknVkMCAAAAAACJFqTIz8+XxYsXS//+/UsHkpxsrs+bNy8WQwIAAAAAAInY3WPv3r1SVFQkjRs3drtdr69evbrM8nl5eeZkL8YBAAAAAACqlpgWzgzU2LFjTYVQ69SiRYtYDwkAAAAAAFSFIEWDBg0kJSVFdu3a5Xa7Xm/SpEmZ5UeOHGlamFinLVu2RHG0AAAAAACgygYp0tPTpUePHjJjxgzXbcXFxeZ6nz59yiyfkZFheqzaTwAAAAAAoGqJSU0Kpe1Hhw0bJj179pRTTz1VXnrpJTly5Ijp9gEAAAAAABJPzIIUV199tezZs0ceffRR2blzp5x00kny1VdflSmmCQAAAAAAEkPMghTqzjvvNCcAAAAAAIBK0d0DAAAAAABUfQQpAAAAAABAXCBIAQAAAAAA4kJMa1KEyuFwmPOcnJxYDwUAgLhhfS9a35OIHPZFAACIzL5IpQxSHDp0yJy3aNEi1kMBACAuvyezsrJiPYwqjX0RAAAisy+S5KiEP7cUFxfL9u3bpVatWpKUlBS2iI/uaGzZskVq164dlnUmKrZl+LAtw4dtGV5sz/jclvqVrjsFzZo1k+RkZnRGEvsi8Y1tGT5sy/BhW4YP2zJ+t2U49kUqZSaF/mObN28ekXXrH4YXeniwLcOHbRk+bMvwYnvG37YkgyI62BepHNiW4cO2DB+2ZfiwLeNzW1Z0X4SfWQAAAAAAQFwgSAEAAAAAAOICQQqnjIwMeeyxx8w5KoZtGT5sy/BhW4YX2zN82Jaw8FoIH7Zl+LAtw4dtGT5sy6q9LStl4UwAAAAAAFD1kEkBAAAAAADiAkEKAAAAAAAQFwhSAAAAAACAuECQAgAAAAAAxAWCFCIybtw4ad26tWRmZkrv3r1lwYIFsR5S3Hv88cclKSnJ7dSxY0fX/bm5uTJ8+HCpX7++1KxZU4YMGSK7du2K6ZjjyXfffScXX3yxNGvWzGy7Tz75xO1+rWf76KOPStOmTaVatWrSv39/Wbdundsy+/fvl6FDh0rt2rWlTp06csstt8jhw4cl0ZS3LW+88cYyr9ULLrjAbRm2ZYmxY8dKr169pFatWtKoUSO59NJLZc2aNW7LBPLe3rx5swwePFiqV69u1vPAAw9IYWGhJJJAtuU555xT5rV5++23uy3Dtkwcib4vEs3Pn1mzZskpp5xiKtm3bdtWJk6cWKX/Hs8884z5fLn33ntdt7EtA7dt2za57rrrzLbSfbKuXbvKokWLwr7P9vPPP8uZZ55ptlOLFi3kueeeKzOWjz76yOxv6zI6ji+++EIqi6KiInnkkUekTZs2ZjudcMIJMmbMGLP9LGzL2B43/ByG7RbIWALiSHCTJ092pKenO9555x3HypUrHbfeequjTp06jl27dsV6aHHtsccec5x44omOHTt2uE579uxx3X/77bc7WrRo4ZgxY4Zj0aJFjtNOO81x+umnx3TM8eSLL75wPPzww46PP/5YP5kdU6ZMcbv/mWeecWRlZTk++eQTx7Jlyxy/+93vHG3atHEcO3bMtcwFF1zg6N69u2P+/PmO77//3tG2bVvHtdde60g05W3LYcOGmW1lf63u37/fbRm2ZYmBAwc6JkyY4FixYoVj6dKljgsvvNDRsmVLx+HDhwN+bxcWFjq6dOni6N+/v+Onn34yf58GDRo4Ro4c6UgkgWzLs88+23zn2F+b2dnZrvvZlomDfZHoff5s2LDBUb16dceIESMcq1atcrz66quOlJQUx1dffVUl/x4LFixwtG7d2tGtWzfHPffc47qdbRkY3V9o1aqV48Ybb3T8+OOP5t/89ddfO3799dew7rPpZ3/jxo0dQ4cONe+Bf//7345q1ao53nzzTdcyP/zwg9m+zz33nNneo0aNcqSlpTmWL1/uqAyeeuopR/369R1Tp051bNy40fHRRx85atas6Xj55Zddy7AtY3fckB2m7RbIWAKR8EGKU0891TF8+HDX9aKiIkezZs0cY8eOjem4KkOQQl/o3hw8eNC8YPXDx/LLL7+YN9W8efOiOMrKwfPDpri42NGkSRPHX//6V7dtmpGRYT4wlH4w6OMWLlzoWubLL790JCUlObZt2+ZIVL6CFJdcconPx7Atfdu9e7fZNrNnzw74va1fpMnJyY6dO3e6lnn99dcdtWvXduTl5TkSlee2tIIU9oMGT2zLxMG+SPQ+f/785z+bH1nsrr76ahMkqWp/j0OHDjnatWvnmDZtmtvnDdsycA8++KDjjDPO8Hl/uPbZXnvtNUfdunXdPtv1uTt06OC6ftVVVzkGDx7s9vy9e/d2/OEPf3BUBjr2m2++2e22yy+/3BwUK7ZlbI8bXgvDdgtkLIFK6Oke+fn5snjxYpOGYklOTjbX582bF9OxVQaauqNpR8cff7xJH9K0QKXbtKCgwG27alpQy5Yt2a4B2Lhxo+zcudNt+2VlZZkUSWv76bmmavXs2dO1jC6vr98ff/wxJuOOZ5qOqqmqHTp0kDvuuEP27dvnuo9t6Vt2drY5r1evXsDvbT3X9L/GjRu7lhk4cKDk5OTIypUrJVF5bkvL+++/Lw0aNJAuXbrIyJEj5ejRo6772JaJgX2R6H7+6DL2dVjLWOuoSn8Pnc6h0zU8/71sy8B9+umnZv/gyiuvNPsRJ598srz99tth32fTZc466yxJT09325Y65enAgQMBbe94d/rpp8uMGTNk7dq15vqyZctkzpw5MmjQIHOdbRmajXG03QIZS6BSJYHt3bvXzI+yfwArvb569eqYjasy0BebzjvUg74dO3bI6NGjzRymFStWmBenvsD1zeC5XfU++GdtI2+vS+s+PdcvS7vU1FSzM8c2dqf1Jy6//HIzB3L9+vXyl7/8xXwh6odlSkoK29KH4uJiM3+5b9++5gBaBfLe1nNvr13rvkTkbVuq//u//5NWrVqZYK/OA33wwQfNzsDHH39s7mdbJgb2RaL7+eNrGT34PnbsmNkZrwp/j8mTJ8uSJUtk4cKFZe5jWwZuw4YN8vrrr8uIESPM/oNuz7vvvttsv2HDhoVtn03PdT/Fcx3WfXXr1vW5vSvL98FDDz1kXhsaENP9L31tPPXUU+aHTsW2DM3OONpugYwlUAkdpEDorKin6tatmwla6M72hx9+aIqkAPHimmuucV3WX4X09arFmjS7ol+/fjEdW7z/AqdBR/2VA5HZlrfddpvba1OLTOlrUoNp+hoFEhWfPxWzZcsWueeee2TatGmmuB0qFjDTX5+ffvppc10zKfS1+cYbb5ggBQKnxwiaPThp0iQ58cQTZenSpSYYqYF6tiU8JfR0D02x1UieZzVjvd6kSZOYjasy0mh8+/bt5ddffzXbTlP8Dh486LYM2zUw1jby97rU8927d7vdrxW3tXIv29g/nZ6k7319rSq2ZVl33nmnTJ06Vb799ltp3ry56/ZA3tt67u21a92XaHxtS2802Kvsr022ZdXHvkh0P398LaMV7/VHlqrw99ApFvq9pl039NdSPc2ePVteeeUVc1l/1WRbBkaDx507d3a7rVOnTq4pzuHaZ6vI9q4s21K7w2g2hf54pIH566+/Xu677z7T2UexLUPTJI62WyBjCVRCByk0VatHjx5mfpQ9YqrX+/TpE9OxVTbawkZ//dMPc92maWlpbttVU5j1A53tWj5NtdI3sn37aXqczhmztp+e686F7ohYZs6caV6/1oEOvNu6daupSaGvVcW2LKX1mPQAYcqUKWYbeKb9BfLe1vPly5e7fRnqr3m60+q5o5fI29Ib/VVJ2V+bbMuqj32R6H7+6DL2dVjLWOuoCn8PzcjS7aCfKdZJswE0rd66zLYMjE458myFqzUVNHs4nPtsuoy2mdRaIfZtqdOqNc0+kO0d77TmktZAsNMglm4HxbYMTZs42m6BjCVgjgSnrZG04ujEiRNN5dPbbrvNtEayVzNGWX/6058cs2bNMi2EtB2NtqjS1lRajdtqbaWtw2bOnGlaW/Xp08ecUFpxW1t66Unfhi+88IK5vGnTJlf7Hn0d/u9//3P8/PPPpjuFt1ZCJ598smmJNWfOHFPBOxHbZvrblnrf/fffb6qV62t1+vTpjlNOOcVsq9zcXNc62JYl7rjjDtM2St/b9raYR48edS1T3nvbals3YMAA00ZQW9E1bNgw4dpmlrcttX3dE088Ybahvjb1vX788cc7zjrrLNc62JaJg32R6H3+WG0zH3jgAdPRYty4cV7bZla1v4dnNyG2ZeAtXFNTU037zHXr1jnef/99829+7733XMuEY59NOyBo+8frr7/etH/U7abP49n+Ucfy/PPPm+2tnfbiuW2mJ+22dtxxx7lakGo7TT120C4xFrZl7I4bDoZpuwUylkAkfJBCaV9n/aDWPs7aKkn7x8I/bTHVtGlTs830A0ev23tG6wvxj3/8o2lloy/wyy67zOxsoMS3335rPmQ8T/oBbrXweeSRR8yHhX659+vXz7FmzRq3dezbt898uGiPaW0JdtNNN5kPsUTjb1vqzq3uYOmOlX6Iaq9z7dHuuXPEtizhbTvqacKECUG9t3/77TfHoEGDTH9t3QHRoGZBQYEjkZS3LTdv3mwCEvXq1TPvce1Xrjv62qfcjm2ZOBJ9XySanz/6vXHSSSeZba3BQftzVNW/h2eQgm0ZuM8++8wEbPSzumPHjo633nrL7f5w7bMtW7bMtDvVdei+tR7sefrwww8d7du3N9tS279+/vnnjsoiJyfHvAb1tZCZmWleLw8//LBby0u2ZWyPG5aFYbsFMpZAJOn/gsu9AAAAAAAACL+ErkkBAAAAAADiB0EKAAAAAAAQFwhSAAAAAACAuECQAgAAAAAAxAWCFAAAAAAAIC4QpAAAAAAAAHGBIAUAAAAAAIgLBCkAAAAAAEBcIEgBAAAAAADiAkEKAAAAAAAQFwhSAAAAAACAuECQAgAAAAAASDz4f6lYUyIkFIc5AAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 2000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "agent.train(num_frames)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Test\n",
    "\n",
    "Run the trained agent (1 episode)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "# load gif and play"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "drl",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
